Merge pull request #2422 from edx/anton/allow-multiple-transcripts
Video: allow multiple transcripts
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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("""<?xml version="1.0" encoding="utf-8" ?>
|
||||
<transcript>
|
||||
<text start="0" dur="0.27"></text>
|
||||
<text start="0.27" dur="2.45">Test text 1.</text>
|
||||
<text start="2.72">Test text 2.</text>
|
||||
<text start="5.43" dur="1.73">Test text 3.</text>
|
||||
</transcript>
|
||||
""")
|
||||
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("""<?xml version="1.0" encoding="utf-8" ?>
|
||||
<transcript>
|
||||
<text start="0" dur="0.27"></text>
|
||||
<text start="0.27" dur="2.45">Test text 1.</text>
|
||||
<text start="2.72">Test text 2.</text>
|
||||
<text start="5.43" dur="1.73">Test text 3.</text>
|
||||
</transcript>
|
||||
""")
|
||||
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'})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
</div>
|
||||
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
|
||||
<a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a>
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
|
||||
<div class="lang menu-container">
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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 @@
|
||||
</div>
|
||||
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
|
||||
<a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a>
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
|
||||
<div class="lang menu-container">
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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 @@
|
||||
</div>
|
||||
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
|
||||
<a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a>
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
|
||||
<div class="lang menu-container">
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -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 @@
|
||||
</div>
|
||||
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
|
||||
<a href="#" class="quality_control" title="HD">HD</a>
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
|
||||
<div class="lang menu-container">
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -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 @@
|
||||
</div>
|
||||
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
|
||||
<a href="#" class="quality_control" title="HD">HD</a>
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
|
||||
<div class="lang menu-container">
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 = $('<ol>'),
|
||||
_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 = $('<ol class="langs-list menu">'),
|
||||
currentLang = this.getCurrentLanguage();
|
||||
|
||||
if (_.keys(languages).length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.videoCaption.showLanguageMenu = true;
|
||||
|
||||
$.each(languages, function(code, label) {
|
||||
var li = $('<li data-lang-code="' + code + '" />'),
|
||||
link = $('<a href="javascript:void(0);">' + label + '</a>');
|
||||
|
||||
if (currentLang === code) {
|
||||
li.addClass('active');
|
||||
}
|
||||
|
||||
li.append(link);
|
||||
menu.append(li);
|
||||
});
|
||||
|
||||
this.videoCaption.container.append(menu);
|
||||
|
||||
menu.on('click', 'a', function (e) {
|
||||
var el = $(e.currentTarget).parent(),
|
||||
Caption = self.videoCaption,
|
||||
langCode = el.data('lang-code');
|
||||
|
||||
if (self.lang !== langCode) {
|
||||
self.lang = langCode;
|
||||
self.storage.setItem('language', langCode);
|
||||
el .addClass('active')
|
||||
.siblings('li')
|
||||
.removeClass('active');
|
||||
|
||||
Caption.fetchCaption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildCaptions (container, captions, start) {
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
$.each(captions, function(index, text) {
|
||||
var liEl = $('<li>');
|
||||
|
||||
liEl.html(text);
|
||||
|
||||
liEl.attr({
|
||||
'data-index': index,
|
||||
'data-start': _this.videoCaption.start[index],
|
||||
'data-start': start[index],
|
||||
'tabindex': 0
|
||||
});
|
||||
|
||||
container.append(liEl);
|
||||
fragment.appendChild(liEl[0]);
|
||||
});
|
||||
|
||||
this.videoCaption.subtitlesEl
|
||||
.html(container.html())
|
||||
.find('li[data-index]')
|
||||
.on({
|
||||
mouseover: this.videoCaption.captionMouseOverOut,
|
||||
mouseout: this.videoCaption.captionMouseOverOut,
|
||||
mousedown: this.videoCaption.captionMouseDown,
|
||||
click: this.videoCaption.captionClick,
|
||||
focus: this.videoCaption.captionFocus,
|
||||
blur: this.videoCaption.captionBlur,
|
||||
keydown: this.videoCaption.captionKeyDown
|
||||
});
|
||||
container.append([fragment]);
|
||||
}
|
||||
|
||||
function renderCaption() {
|
||||
var Caption = this.videoCaption,
|
||||
events = ['mouseover', 'mouseout', 'mousedown', 'click', 'focus',
|
||||
'blur', 'keydown'].join(' ');
|
||||
|
||||
Caption.setSubtitlesHeight();
|
||||
|
||||
buildCaptions(Caption.subtitlesEl, Caption.captions, Caption.start);
|
||||
|
||||
Caption.subtitlesEl.on(events, 'li[data-index]', function (event) {
|
||||
switch (event.type) {
|
||||
case 'mouseover':
|
||||
case 'mouseout':
|
||||
Caption.captionMouseOverOut(event);
|
||||
break;
|
||||
case 'mousedown':
|
||||
Caption.captionMouseDown(event);
|
||||
break;
|
||||
case 'click':
|
||||
Caption.captionClick(event);
|
||||
break;
|
||||
case 'focusin':
|
||||
Caption.captionFocus(event);
|
||||
break;
|
||||
case 'focusout':
|
||||
Caption.captionBlur(event);
|
||||
break;
|
||||
case 'keydown':
|
||||
Caption.captionKeyDown(event);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Enables or disables automatic scrolling of the captions when the
|
||||
// video is playing. This feature has to be disabled when tabbing
|
||||
// through them as it interferes with that action. Initially, have this
|
||||
// flag enabled as we assume mouse use. Then, if the first caption
|
||||
// (through forward tabbing) or the last caption (through backwards
|
||||
// tabbing) gets the focus, disable that feature. Renable it if tabbing
|
||||
// tabbing) gets the focus, disable that feature. Re-enable it if tabbing
|
||||
// then cycles out of the the captions.
|
||||
this.videoCaption.autoScrolling = true;
|
||||
Caption.autoScrolling = true;
|
||||
// Keeps track of where the focus is situated in the array of captions.
|
||||
// Used to implement the automatic scrolling behavior and decide if the
|
||||
// outline around a caption has to be hidden or shown on a mouseenter
|
||||
// or mouseleave. Initially, no caption has the focus, set the
|
||||
// index to -1.
|
||||
this.videoCaption.currentCaptionIndex = -1;
|
||||
Caption.currentCaptionIndex = -1;
|
||||
// Used to track if the focus is coming from a click or tabbing. This
|
||||
// has to be known to decide if, when a caption gets the focus, an
|
||||
// outline has to be drawn (tabbing) or not (mouse click).
|
||||
this.videoCaption.isMouseFocus = false;
|
||||
Caption.isMouseFocus = false;
|
||||
Caption.addPaddings();
|
||||
Caption.rendered = true;
|
||||
}
|
||||
|
||||
// Set top and bottom spacing heigh and make sure they are taken out of
|
||||
function reRenderCaption() {
|
||||
var Caption = this.videoCaption;
|
||||
|
||||
Caption.currentIndex = null;
|
||||
Caption.rendered = false;
|
||||
Caption.subtitlesEl.empty();
|
||||
buildCaptions(Caption.subtitlesEl, Caption.captions, Caption.start);
|
||||
Caption.addPaddings();
|
||||
Caption.rendered = true;
|
||||
}
|
||||
|
||||
function addPaddings() {
|
||||
// Set top and bottom spacing height and make sure they are taken out of
|
||||
// the tabbing order.
|
||||
this.videoCaption.subtitlesEl
|
||||
.prepend(
|
||||
@@ -426,8 +483,6 @@ function () {
|
||||
.height(this.videoCaption.bottomSpacingHeight())
|
||||
.attr('tabindex', -1)
|
||||
);
|
||||
|
||||
this.videoCaption.rendered = true;
|
||||
}
|
||||
|
||||
// On mouseOver, hide the outline of a caption that has been tabbed to.
|
||||
@@ -487,6 +542,7 @@ function () {
|
||||
function captionBlur(event) {
|
||||
var caption = $(event.target),
|
||||
captionIndex = parseInt(caption.attr('data-index'), 10);
|
||||
|
||||
caption.removeClass('focused');
|
||||
// If we are on first or last index, we have to turn automatic scroll
|
||||
// on again when losing focus. There is no way to know in what
|
||||
@@ -494,8 +550,7 @@ function () {
|
||||
// tabbing back out of the captions or on the last element and tabbing
|
||||
// forward out of the captions.
|
||||
if (captionIndex === 0 ||
|
||||
captionIndex === this.videoCaption.captions.length-1) {
|
||||
this.videoCaption.autoHideCaptions();
|
||||
captionIndex === this.videoCaption.captions.length - 1) {
|
||||
|
||||
this.videoCaption.autoScrolling = true;
|
||||
}
|
||||
@@ -661,25 +716,9 @@ function () {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.el.hasClass('closed')) {
|
||||
this.videoCaption.autoShowCaptions();
|
||||
this.videoCaption.hideCaptions(false);
|
||||
} else {
|
||||
this.videoCaption.hideCaptions(true);
|
||||
|
||||
// In the case when captions are not auto-hidden based on mouse
|
||||
// movement anywhere on the video, we must hide them explicitly
|
||||
// after the "CC" button has been clicked (to hide captions).
|
||||
//
|
||||
// Otherwise, in order for the captions to disappear again, the
|
||||
// user must move the mouse button over the "CC" button, or over
|
||||
// the captions themselves. In this case, an "autoShow" will be
|
||||
// triggered, and after a timeout, an "autoHide".
|
||||
if (!this.config.autohideHtml5) {
|
||||
this.captionHideTimeout = setTimeout(
|
||||
this.videoCaption.autoHideCaptions(),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,31 +790,23 @@ function () {
|
||||
|
||||
function setSubtitlesHeight() {
|
||||
var height = 0;
|
||||
if (
|
||||
((this.videoType === 'html5') && (this.config.autohideHtml5)) ||
|
||||
(!this.config.autohideHtml5)
|
||||
){
|
||||
// on page load captionHidden = undefined
|
||||
if (
|
||||
(
|
||||
this.captionsHidden === undefined &&
|
||||
this.hide_captions === true
|
||||
) ||
|
||||
(this.captionsHidden === true)
|
||||
) {
|
||||
// In case of html5 autoshowing subtitles, we adjust height of
|
||||
// subs, by height of scrollbar.
|
||||
height = this.videoControl.el.height() +
|
||||
0.5 * this.videoControl.sliderEl.height();
|
||||
// Height of videoControl does not contain height of slider.
|
||||
// css is set to absolute, to avoid yanking when slider
|
||||
// autochanges its height.
|
||||
}
|
||||
// on page load captionHidden = undefined
|
||||
if ((this.captionsHidden === undefined && this.hide_captions) ||
|
||||
this.captionsHidden === true
|
||||
) {
|
||||
// In case of html5 autoshowing subtitles, we adjust height of
|
||||
// subs, by height of scrollbar.
|
||||
height = this.videoControl.el.height() +
|
||||
0.5 * this.videoControl.sliderEl.height();
|
||||
// Height of videoControl does not contain height of slider.
|
||||
// css is set to absolute, to avoid yanking when slider
|
||||
// autochanges its height.
|
||||
}
|
||||
|
||||
this.videoCaption.subtitlesEl.css({
|
||||
maxHeight: this.videoCaption.captionHeight() - height
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
|
||||
@@ -113,7 +113,7 @@ class ModelsTest(unittest.TestCase):
|
||||
|
||||
def test_load_class(self):
|
||||
vc = XModuleDescriptor.load_class('video')
|
||||
vc_str = "<class 'xmodule.video_module.VideoDescriptor'>"
|
||||
vc_str = "<class 'xmodule.video_module.video_module.VideoDescriptor'>"
|
||||
self.assertEqual(str(vc), vc_str)
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from mock import Mock
|
||||
from . import LogicTest
|
||||
from lxml import etree
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.video_module import VideoDescriptor, _create_youtube_string
|
||||
from xmodule.video_module import VideoDescriptor, create_youtube_string
|
||||
from .test_import import DummySystem
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds
|
||||
@@ -150,7 +150,7 @@ class VideoDescriptorTest(unittest.TestCase):
|
||||
descriptor.youtube_id_1_25 = '1EeWXzPdhSA'
|
||||
descriptor.youtube_id_1_5 = 'rABDYkeK0x8'
|
||||
expected = "0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"
|
||||
self.assertEqual(_create_youtube_string(descriptor), expected)
|
||||
self.assertEqual(create_youtube_string(descriptor), expected)
|
||||
|
||||
def test_create_youtube_string_missing(self):
|
||||
"""
|
||||
@@ -165,7 +165,7 @@ class VideoDescriptorTest(unittest.TestCase):
|
||||
descriptor.youtube_id_1_0 = 'p2Q6BrNhdh8'
|
||||
descriptor.youtube_id_1_25 = '1EeWXzPdhSA'
|
||||
expected = "0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA"
|
||||
self.assertEqual(_create_youtube_string(descriptor), expected)
|
||||
self.assertEqual(create_youtube_string(descriptor), expected)
|
||||
|
||||
|
||||
class VideoDescriptorImportTestCase(unittest.TestCase):
|
||||
@@ -193,6 +193,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
|
||||
<source src="http://www.example.com/source.mp4"/>
|
||||
<source src="http://www.example.com/source.ogg"/>
|
||||
<track src="http://www.example.com/track"/>
|
||||
<transcript language="ua" src="ukrainian_translation.srt" />
|
||||
<transcript language="ge" src="german_translation.srt" />
|
||||
</video>
|
||||
'''
|
||||
location = Location(["i4x", "edX", "video", "default",
|
||||
@@ -215,7 +217,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
|
||||
'track': 'http://www.example.com/track',
|
||||
'download_track': True,
|
||||
'html5_sources': ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg'],
|
||||
'data': ''
|
||||
'data': '',
|
||||
'transcripts': {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'}
|
||||
})
|
||||
|
||||
def test_from_xml(self):
|
||||
@@ -230,6 +233,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
|
||||
end_time="00:01:00">
|
||||
<source src="http://www.example.com/source.mp4"/>
|
||||
<track src="http://www.example.com/track"/>
|
||||
<transcript language="ua" src="ukrainian_translation.srt" />
|
||||
<transcript language="ge" src="german_translation.srt" />
|
||||
</video>
|
||||
'''
|
||||
output = VideoDescriptor.from_xml(xml_data, module_system, Mock())
|
||||
@@ -245,7 +250,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
|
||||
'download_track': False,
|
||||
'download_video': False,
|
||||
'html5_sources': ['http://www.example.com/source.mp4'],
|
||||
'data': ''
|
||||
'data': '',
|
||||
'transcripts': {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'},
|
||||
})
|
||||
|
||||
def test_from_xml_missing_attributes(self):
|
||||
@@ -304,7 +310,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
|
||||
'download_track': True,
|
||||
'download_video': True,
|
||||
'html5_sources': ['http://www.example.com/source.mp4'],
|
||||
'data': ''
|
||||
'data': '',
|
||||
'transcripts': {},
|
||||
})
|
||||
|
||||
def test_from_xml_no_attributes(self):
|
||||
@@ -326,7 +333,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
|
||||
'download_track': False,
|
||||
'download_video': False,
|
||||
'html5_sources': [],
|
||||
'data': ''
|
||||
'data': '',
|
||||
'transcripts': {},
|
||||
})
|
||||
|
||||
def test_from_xml_double_quotes(self):
|
||||
@@ -508,6 +516,7 @@ class VideoExportTestCase(unittest.TestCase):
|
||||
desc.download_track = True
|
||||
desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg']
|
||||
desc.download_video = True
|
||||
desc.transcripts = {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'}
|
||||
|
||||
xml = desc.definition_to_xml(None) # We don't use the `resource_fs` parameter
|
||||
expected = etree.fromstring('''\
|
||||
@@ -515,9 +524,10 @@ class VideoExportTestCase(unittest.TestCase):
|
||||
<source src="http://www.example.com/source.mp4"/>
|
||||
<source src="http://www.example.com/source.ogg"/>
|
||||
<track src="http://www.example.com/track"/>
|
||||
<transcript language="ge" src="german_translation.srt" />
|
||||
<transcript language="ua" src="ukrainian_translation.srt" />
|
||||
</video>
|
||||
''')
|
||||
|
||||
self.assertXmlEqual(expected, xml)
|
||||
|
||||
def test_export_to_xml_empty_end_time(self):
|
||||
|
||||
10
common/lib/xmodule/xmodule/video_module/__init__.py
Normal file
10
common/lib/xmodule/xmodule/video_module/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Container for video module and it's utils.
|
||||
"""
|
||||
|
||||
# Disable wildcard-import warnings.
|
||||
# pylint: disable=W0401
|
||||
|
||||
from .transcripts_utils import *
|
||||
from .video_utils import *
|
||||
from .video_module import *
|
||||
@@ -2,6 +2,7 @@
|
||||
Utility functions for transcripts.
|
||||
++++++++++++++++++++++++++++++++++
|
||||
"""
|
||||
import os
|
||||
import copy
|
||||
import json
|
||||
import requests
|
||||
@@ -9,29 +10,27 @@ import logging
|
||||
from pysrt import SubRipTime, SubRipItem, SubRipFile
|
||||
from lxml import etree
|
||||
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from .utils import get_modulestore
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TranscriptsGenerationException(Exception):
|
||||
class TranscriptException(Exception): # pylint disable=C0111
|
||||
pass
|
||||
|
||||
|
||||
class GetTranscriptsFromYouTubeException(Exception):
|
||||
class TranscriptsGenerationException(Exception): # pylint disable=C0111
|
||||
pass
|
||||
|
||||
|
||||
class TranscriptsRequestValidationException(Exception):
|
||||
class GetTranscriptsFromYouTubeException(Exception): # pylint disable=C0111
|
||||
pass
|
||||
|
||||
|
||||
class TranscriptsRequestValidationException(Exception): # pylint disable=C0111
|
||||
pass
|
||||
|
||||
|
||||
@@ -42,7 +41,7 @@ def generate_subs(speed, source_speed, source_subs):
|
||||
Args:
|
||||
`speed`: float, for this speed subtitles will be generated,
|
||||
`source_speed`: float, speed of source_subs
|
||||
`soource_subs`: dict, existing subtitles for speed `source_speed`.
|
||||
`source_subs`: dict, existing subtitles for speed `source_speed`.
|
||||
|
||||
Returns:
|
||||
`subs`: dict, actual subtitles.
|
||||
@@ -64,30 +63,27 @@ def generate_subs(speed, source_speed, source_subs):
|
||||
return subs
|
||||
|
||||
|
||||
def save_subs_to_store(subs, subs_id, item):
|
||||
def save_subs_to_store(subs, subs_id, item, language='en'):
|
||||
"""
|
||||
Save transcripts into `StaticContent`.
|
||||
|
||||
Args:
|
||||
`subs_id`: str, subtitles id
|
||||
`item`: video module instance
|
||||
`language`: two chars str ('uk'), language of translation of transcripts
|
||||
|
||||
Returns: location of saved subtitles.
|
||||
"""
|
||||
filedata = json.dumps(subs, indent=2)
|
||||
mime_type = 'application/json'
|
||||
filename = 'subs_{0}.srt.sjson'.format(subs_id)
|
||||
|
||||
content_location = StaticContent.compute_location(
|
||||
item.location.org, item.location.course, filename
|
||||
)
|
||||
filename = subs_filename(subs_id, language)
|
||||
content_location = asset_location(item.location, filename)
|
||||
content = StaticContent(content_location, filename, mime_type, filedata)
|
||||
contentstore().save(content)
|
||||
del_cached_content(content_location)
|
||||
return content_location
|
||||
|
||||
|
||||
def get_transcripts_from_youtube(youtube_id):
|
||||
def get_transcripts_from_youtube(youtube_id, settings, i18n):
|
||||
"""
|
||||
Gets transcripts from youtube for youtube_id.
|
||||
|
||||
@@ -96,6 +92,8 @@ def get_transcripts_from_youtube(youtube_id):
|
||||
|
||||
Returns (status, transcripts): bool, dict.
|
||||
"""
|
||||
_ = i18n.ugettext
|
||||
|
||||
utf8_parser = etree.XMLParser(encoding='utf-8')
|
||||
|
||||
youtube_api = copy.deepcopy(settings.YOUTUBE_API)
|
||||
@@ -127,7 +125,7 @@ def get_transcripts_from_youtube(youtube_id):
|
||||
return {'start': sub_starts, 'end': sub_ends, 'text': sub_texts}
|
||||
|
||||
|
||||
def download_youtube_subs(youtube_subs, item):
|
||||
def download_youtube_subs(youtube_subs, item, settings):
|
||||
"""
|
||||
Download transcripts from Youtube and save them to assets.
|
||||
|
||||
@@ -138,6 +136,9 @@ def download_youtube_subs(youtube_subs, item):
|
||||
Returns: None, if transcripts were successfully downloaded and saved.
|
||||
Otherwise raises GetTranscriptsFromYouTubeException.
|
||||
"""
|
||||
i18n = item.runtime.service(item, "i18n")
|
||||
_ = i18n.ugettext
|
||||
|
||||
highest_speed = highest_speed_subs = None
|
||||
missed_speeds = []
|
||||
# Iterate from lowest to highest speed and try to do download transcripts
|
||||
@@ -146,7 +147,7 @@ def download_youtube_subs(youtube_subs, item):
|
||||
if not youtube_id:
|
||||
continue
|
||||
try:
|
||||
subs = get_transcripts_from_youtube(youtube_id)
|
||||
subs = get_transcripts_from_youtube(youtube_id, settings, i18n)
|
||||
if not subs: # if empty subs are returned
|
||||
raise GetTranscriptsFromYouTubeException
|
||||
except GetTranscriptsFromYouTubeException:
|
||||
@@ -187,24 +188,19 @@ def download_youtube_subs(youtube_subs, item):
|
||||
)
|
||||
|
||||
|
||||
def remove_subs_from_store(subs_id, item):
|
||||
def remove_subs_from_store(subs_id, item, lang='en'):
|
||||
"""
|
||||
Remove from store, if transcripts content exists.
|
||||
"""
|
||||
filename = 'subs_{0}.srt.sjson'.format(subs_id)
|
||||
content_location = StaticContent.compute_location(
|
||||
item.location.org, item.location.course, filename
|
||||
)
|
||||
try:
|
||||
content = contentstore().find(content_location)
|
||||
content = asset(item.location, subs_id, lang)
|
||||
contentstore().delete(content.get_id())
|
||||
del_cached_content(content.location)
|
||||
log.info("Removed subs %s from store", subs_id)
|
||||
except NotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def generate_subs_from_source(speed_subs, subs_type, subs_filedata, item):
|
||||
def generate_subs_from_source(speed_subs, subs_type, subs_filedata, item, language='en'):
|
||||
"""Generate transcripts from source files (like SubRip format, etc.)
|
||||
and save them to assets for `item` module.
|
||||
We expect, that speed of source subs equal to 1
|
||||
@@ -213,15 +209,17 @@ def generate_subs_from_source(speed_subs, subs_type, subs_filedata, item):
|
||||
:param subs_type: type of source subs: "srt", ...
|
||||
:param subs_filedata:unicode, content of source subs.
|
||||
:param item: module object.
|
||||
:param language: str, language of translation of transcripts
|
||||
:returns: True, if all subs are generated and saved successfully.
|
||||
"""
|
||||
_ = item.runtime.service(item, "i18n").ugettext
|
||||
if subs_type != 'srt':
|
||||
raise TranscriptsGenerationException(_("We support only SubRip (*.srt) transcripts format."))
|
||||
try:
|
||||
srt_subs_obj = SubRipFile.from_string(subs_filedata)
|
||||
except Exception as e:
|
||||
except Exception as ex:
|
||||
msg = _("Something wrong with SubRip transcripts file during parsing. Inner message is {error_message}").format(
|
||||
error_message=e.message
|
||||
error_message=ex.message
|
||||
)
|
||||
raise TranscriptsGenerationException(msg)
|
||||
if not srt_subs_obj:
|
||||
@@ -245,7 +243,8 @@ def generate_subs_from_source(speed_subs, subs_type, subs_filedata, item):
|
||||
save_subs_to_store(
|
||||
generate_subs(speed, 1, subs),
|
||||
subs_id,
|
||||
item
|
||||
item,
|
||||
language
|
||||
)
|
||||
|
||||
return subs
|
||||
@@ -279,15 +278,6 @@ def generate_srt_from_sjson(sjson_subs, speed):
|
||||
return output
|
||||
|
||||
|
||||
def save_module(item, user):
|
||||
"""
|
||||
Proceed with additional save operations.
|
||||
"""
|
||||
item.save()
|
||||
store = get_modulestore(Location(item.id))
|
||||
store.update_item(item, user.id if user else None)
|
||||
|
||||
|
||||
def copy_or_rename_transcript(new_name, old_name, item, delete_old=False, user=None):
|
||||
"""
|
||||
Renames `old_name` transcript file in storage to `new_name`.
|
||||
@@ -302,7 +292,7 @@ def copy_or_rename_transcript(new_name, old_name, item, delete_old=False, user=N
|
||||
transcripts = contentstore().find(content_location).data
|
||||
save_subs_to_store(json.loads(transcripts), new_name, item)
|
||||
item.sub = new_name
|
||||
save_module(item, user)
|
||||
item.save_with_metadata(user)
|
||||
if delete_old:
|
||||
remove_subs_from_store(old_name, item)
|
||||
|
||||
@@ -316,7 +306,7 @@ def get_html5_ids(html5_sources):
|
||||
return html5_ids
|
||||
|
||||
|
||||
def manage_video_subtitles_save(old_item, new_item, user):
|
||||
def manage_video_subtitles_save(item, user, old_metadata=None, generate_translation=False):
|
||||
"""
|
||||
Does some specific things, that can be done only on save.
|
||||
|
||||
@@ -324,6 +314,12 @@ def manage_video_subtitles_save(old_item, new_item, user):
|
||||
|
||||
If value of `sub` field of `new_item` is cleared, transcripts should be removed.
|
||||
|
||||
`item` is video module instance with updated values of fields,
|
||||
but actually have not been saved to store yet.
|
||||
|
||||
`old_metadata` contains old values of XFields.
|
||||
|
||||
# 1.
|
||||
If value of `sub` field of `new_item` is different from values of video fields of `new_item`,
|
||||
and `new_item.sub` file is present, then code in this function creates copies of
|
||||
`new_item.sub` file with new names. That names are equal to values of video fields of `new_item`
|
||||
@@ -331,23 +327,28 @@ def manage_video_subtitles_save(old_item, new_item, user):
|
||||
This whole action ensures that after user changes video fields, proper `sub` files, corresponding
|
||||
to new values of video fields, will be presented in system.
|
||||
|
||||
old_item is not used here, but is added for future changes.
|
||||
# 2. Generate transcripts translation only when user clicks `save` button, not while switching tabs.
|
||||
a) delete sjson translation for those languages, which were removed from `item.transcripts`.
|
||||
Note: we are not deleting old SRT files to give user more flexibility.
|
||||
b) For all SRT files in`item.transcripts` regenerate new SJSON files.
|
||||
(To avoid confusing situation if you attempt to correct a translation by uploading
|
||||
a new version of the SRT file with same name).
|
||||
"""
|
||||
|
||||
# 1.
|
||||
html5_ids = get_html5_ids(new_item.html5_sources)
|
||||
possible_video_id_list = [new_item.youtube_id_1_0] + html5_ids
|
||||
sub_name = new_item.sub
|
||||
html5_ids = get_html5_ids(item.html5_sources)
|
||||
possible_video_id_list = [item.youtube_id_1_0] + html5_ids
|
||||
sub_name = item.sub
|
||||
for video_id in possible_video_id_list:
|
||||
if not video_id:
|
||||
continue
|
||||
if not sub_name:
|
||||
remove_subs_from_store(video_id, new_item)
|
||||
remove_subs_from_store(video_id, item)
|
||||
continue
|
||||
# copy_or_rename_transcript changes item.sub of module
|
||||
try:
|
||||
# updates item.sub with `video_id`, if it is successful.
|
||||
copy_or_rename_transcript(video_id, sub_name, new_item, user=user)
|
||||
copy_or_rename_transcript(video_id, sub_name, item, user=user)
|
||||
except NotFoundError:
|
||||
# subtitles file `sub_name` is not presented in the system. Nothing to copy or rename.
|
||||
log.debug(
|
||||
@@ -355,3 +356,121 @@ def manage_video_subtitles_save(old_item, new_item, user):
|
||||
"original file does not exist.",
|
||||
sub_name, video_id
|
||||
)
|
||||
|
||||
# 2.
|
||||
if generate_translation:
|
||||
old_langs = set(old_metadata.get('transcripts', {})) if old_metadata else set()
|
||||
new_langs = set(item.transcripts)
|
||||
|
||||
for lang in old_langs.difference(new_langs): # 2a
|
||||
for video_id in possible_video_id_list:
|
||||
if video_id:
|
||||
remove_subs_from_store(video_id, item, lang)
|
||||
|
||||
reraised_message = ''
|
||||
for lang in new_langs: # 2b
|
||||
try:
|
||||
generate_sjson_for_all_speeds(
|
||||
item,
|
||||
item.transcripts[lang],
|
||||
{speed: subs_id for subs_id, speed in youtube_speed_dict(item).iteritems()},
|
||||
lang,
|
||||
)
|
||||
except TranscriptException as ex:
|
||||
item.transcripts.pop(lang) # remove key from transcripts because proper srt file does not exist in assets.
|
||||
reraised_message += ' ' + ex.message
|
||||
if reraised_message:
|
||||
item.save_with_metadata(user)
|
||||
raise TranscriptException(reraised_message)
|
||||
|
||||
|
||||
def youtube_speed_dict(item):
|
||||
"""
|
||||
Returns {speed: youtube_ids, ...} dict for existing youtube_ids
|
||||
"""
|
||||
yt_ids = [item.youtube_id_0_75, item.youtube_id_1_0, item.youtube_id_1_25, item.youtube_id_1_5]
|
||||
yt_speeds = [0.75, 1.00, 1.25, 1.50]
|
||||
youtube_ids = {p[0]: p[1] for p in zip(yt_ids, yt_speeds) if p[0]}
|
||||
return youtube_ids
|
||||
|
||||
|
||||
def subs_filename(subs_id, lang='en'):
|
||||
"""
|
||||
Generate proper filename for storage.
|
||||
"""
|
||||
if lang == 'en':
|
||||
return 'subs_{0}.srt.sjson'.format(subs_id)
|
||||
else:
|
||||
return '{0}_subs_{1}.srt.sjson'.format(lang, subs_id)
|
||||
|
||||
|
||||
def asset_location(location, filename):
|
||||
"""
|
||||
Return asset location.
|
||||
|
||||
`location` is module location.
|
||||
"""
|
||||
return StaticContent.compute_location(
|
||||
location.org, location.course, filename
|
||||
)
|
||||
|
||||
|
||||
def asset(location, subs_id, lang='en', filename=None):
|
||||
"""
|
||||
Get asset from contentstore, asset location is built from subs_id and lang.
|
||||
|
||||
`location` is module location.
|
||||
"""
|
||||
return contentstore().find(
|
||||
asset_location(
|
||||
location,
|
||||
subs_filename(subs_id, lang) if not filename else filename
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang):
|
||||
"""
|
||||
Generates sjson from srt for given lang.
|
||||
|
||||
`item` is module object.
|
||||
"""
|
||||
try:
|
||||
srt_transcripts = contentstore().find(asset_location(item.location, user_filename))
|
||||
except NotFoundError as ex:
|
||||
raise TranscriptException("{}: Can't find uploaded transcripts: {}".format(ex.message, user_filename))
|
||||
|
||||
if not lang:
|
||||
lang = item.transcript_language
|
||||
|
||||
generate_subs_from_source(
|
||||
result_subs_dict,
|
||||
os.path.splitext(user_filename)[1][1:],
|
||||
srt_transcripts.data.decode('utf8'),
|
||||
item,
|
||||
lang
|
||||
)
|
||||
|
||||
|
||||
def get_or_create_sjson(item):
|
||||
"""
|
||||
Get sjson if already exists, otherwise generate it.
|
||||
|
||||
Generate sjson with subs_id name, from user uploaded srt.
|
||||
Subs_id is extracted from srt filename, which was set by user.
|
||||
|
||||
Raises:
|
||||
TranscriptException: when srt subtitles do not exist,
|
||||
and exceptions from generate_subs_from_source.
|
||||
|
||||
`item` is module object.
|
||||
"""
|
||||
user_filename = item.transcripts[item.transcript_language]
|
||||
user_subs_id = os.path.splitext(user_filename)[0]
|
||||
source_subs_id, result_subs_dict = user_subs_id, {1.0: user_subs_id}
|
||||
try:
|
||||
sjson_transcript = asset(item.location, source_subs_id, item.transcript_language).data
|
||||
except (NotFoundError): # generating sjson from srt
|
||||
generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, item.transcript_language)
|
||||
sjson_transcript = asset(item.location, source_subs_id, item.transcript_language).data
|
||||
return sjson_transcript
|
||||
@@ -10,15 +10,17 @@ in-browser HTML5 video method (when in HTML5 mode).
|
||||
in XML.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from operator import itemgetter
|
||||
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string
|
||||
import datetime
|
||||
import copy
|
||||
from webob import Response
|
||||
from pysrt import SubRipTime, SubRipItem
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
@@ -26,12 +28,19 @@ from xmodule.x_module import XModule, module_attr
|
||||
from xmodule.editing_module import TabsEditingDescriptor
|
||||
from xmodule.raw_module import EmptyDataRawDescriptor
|
||||
from xmodule.xml_module import is_pointer_tag, name_to_pathname, deserialize_field
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Scope, String, Float, Boolean, List, ScopeIds
|
||||
from xblock.fields import Scope, String, Float, Boolean, List, Dict, ScopeIds
|
||||
from xmodule.fields import RelativeTime
|
||||
from .transcripts_utils import (
|
||||
generate_srt_from_sjson,
|
||||
asset,
|
||||
get_or_create_sjson,
|
||||
TranscriptException,
|
||||
generate_sjson_for_all_speeds,
|
||||
youtube_speed_dict
|
||||
)
|
||||
from .video_utils import create_youtube_string
|
||||
|
||||
from xmodule.modulestore.inheritance import InheritanceKeyValueStore
|
||||
from xblock.runtime import KvsFieldData
|
||||
@@ -51,12 +60,6 @@ class VideoFields(object):
|
||||
scope=Scope.user_state,
|
||||
default=datetime.timedelta(seconds=0)
|
||||
)
|
||||
show_captions = Boolean(
|
||||
help="This controls whether or not captions are shown by default.",
|
||||
display_name="Show Transcript",
|
||||
scope=Scope.settings,
|
||||
default=True
|
||||
)
|
||||
# TODO: This should be moved to Scope.content, but this will
|
||||
# require data migration to support the old video module.
|
||||
youtube_id_1_0 = String(
|
||||
@@ -130,10 +133,29 @@ class VideoFields(object):
|
||||
)
|
||||
sub = String(
|
||||
help="The name of the timed transcript track (for non-Youtube videos).",
|
||||
display_name="HTML5 Transcript",
|
||||
display_name="Transcript (primary)",
|
||||
scope=Scope.settings,
|
||||
default=""
|
||||
)
|
||||
show_captions = Boolean(
|
||||
help="This controls whether or not captions are shown by default.",
|
||||
display_name="Transcript Display",
|
||||
scope=Scope.settings,
|
||||
default=True
|
||||
)
|
||||
# Data format: {'de': 'german_translation', 'uk': 'ukrainian_translation'}
|
||||
transcripts = Dict(
|
||||
help="Add additional transcripts in other languages",
|
||||
display_name="Transcript Translations",
|
||||
scope=Scope.settings,
|
||||
default={}
|
||||
)
|
||||
transcript_language = String(
|
||||
help="Preferred language for transcript",
|
||||
display_name="Preferred language for transcript",
|
||||
scope=Scope.preferences,
|
||||
default="en"
|
||||
)
|
||||
speed = Float(
|
||||
help="The last speed that was explicitly set by user for the video.",
|
||||
scope=Scope.user_state,
|
||||
@@ -163,30 +185,31 @@ class VideoModule(VideoFields, XModule):
|
||||
|
||||
# To make sure that js files are called in proper order we use numerical
|
||||
# index. We do that to avoid issues that occurs in tests.
|
||||
module = __name__.replace('.video_module', '', 2)
|
||||
js = {
|
||||
'js': [
|
||||
resource_string(__name__, 'js/src/video/00_video_storage.js'),
|
||||
resource_string(__name__, 'js/src/video/00_resizer.js'),
|
||||
resource_string(__name__, 'js/src/video/01_initialize.js'),
|
||||
resource_string(__name__, 'js/src/video/025_focus_grabber.js'),
|
||||
resource_string(__name__, 'js/src/video/02_html5_video.js'),
|
||||
resource_string(__name__, 'js/src/video/03_video_player.js'),
|
||||
resource_string(__name__, 'js/src/video/04_video_control.js'),
|
||||
resource_string(__name__, 'js/src/video/05_video_quality_control.js'),
|
||||
resource_string(__name__, 'js/src/video/06_video_progress_slider.js'),
|
||||
resource_string(__name__, 'js/src/video/07_video_volume_control.js'),
|
||||
resource_string(__name__, 'js/src/video/08_video_speed_control.js'),
|
||||
resource_string(__name__, 'js/src/video/09_video_caption.js'),
|
||||
resource_string(__name__, 'js/src/video/10_main.js')
|
||||
resource_string(module, 'js/src/video/00_video_storage.js'),
|
||||
resource_string(module, 'js/src/video/00_resizer.js'),
|
||||
resource_string(module, 'js/src/video/01_initialize.js'),
|
||||
resource_string(module, 'js/src/video/025_focus_grabber.js'),
|
||||
resource_string(module, 'js/src/video/02_html5_video.js'),
|
||||
resource_string(module, 'js/src/video/03_video_player.js'),
|
||||
resource_string(module, 'js/src/video/04_video_control.js'),
|
||||
resource_string(module, 'js/src/video/05_video_quality_control.js'),
|
||||
resource_string(module, 'js/src/video/06_video_progress_slider.js'),
|
||||
resource_string(module, 'js/src/video/07_video_volume_control.js'),
|
||||
resource_string(module, 'js/src/video/08_video_speed_control.js'),
|
||||
resource_string(module, 'js/src/video/09_video_caption.js'),
|
||||
resource_string(module, 'js/src/video/10_main.js')
|
||||
]
|
||||
}
|
||||
css = {'scss': [resource_string(__name__, 'css/video/display.scss')]}
|
||||
css = {'scss': [resource_string(module, 'css/video/display.scss')]}
|
||||
js_module_name = "Video"
|
||||
|
||||
def handle_ajax(self, dispatch, data):
|
||||
accepted_keys = ['speed', 'saved_video_position']
|
||||
|
||||
accepted_keys = ['speed', 'saved_video_position', 'transcript_language']
|
||||
if dispatch == 'save_user_state':
|
||||
|
||||
for key in data:
|
||||
if hasattr(self, key) and key in accepted_keys:
|
||||
if key == 'saved_video_position':
|
||||
@@ -206,7 +229,6 @@ class VideoModule(VideoFields, XModule):
|
||||
|
||||
def get_html(self):
|
||||
track_url = None
|
||||
caption_asset_path = "/static/subs/"
|
||||
|
||||
get_ext = lambda filename: filename.rpartition('.')[-1]
|
||||
sources = {get_ext(src): src for src in self.html5_sources}
|
||||
@@ -221,7 +243,26 @@ class VideoModule(VideoFields, XModule):
|
||||
if self.track:
|
||||
track_url = self.track
|
||||
elif self.sub:
|
||||
track_url = self.runtime.handler_url(self, 'download_transcript')
|
||||
track_url = self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/download'
|
||||
|
||||
if self.transcript_language in self.transcripts:
|
||||
transcript_language = self.transcript_language
|
||||
elif self.sub:
|
||||
transcript_language = 'en'
|
||||
elif self.transcripts:
|
||||
transcript_language = self.transcripts.keys()[0]
|
||||
else:
|
||||
# this for the case, when for currently selected video,
|
||||
# there are no translations and English subtitles are not set by instructor.
|
||||
transcript_language = 'null'
|
||||
|
||||
all_languages = {i[0]: i[1] for i in settings.ALL_LANGUAGES}
|
||||
languages = {lang: all_languages[lang] for lang in self.transcripts}
|
||||
if self.sub:
|
||||
languages.update({'en': 'English'})
|
||||
|
||||
# OrderedDict for easy testing of rendered context in tests
|
||||
transcript_languages = OrderedDict(sorted(languages.items(), key=itemgetter(1)))
|
||||
|
||||
return self.system.render_template('video.html', {
|
||||
'ajax_url': self.system.ajax_url + '/save_user_state',
|
||||
@@ -230,7 +271,6 @@ class VideoModule(VideoFields, XModule):
|
||||
# isn't on the filesystem
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
'display_name': self.display_name_with_default,
|
||||
'caption_asset_path': caption_asset_path,
|
||||
'end': self.end_time.total_seconds(),
|
||||
'id': self.location.html_id(),
|
||||
'show_captions': json.dumps(self.show_captions),
|
||||
@@ -241,68 +281,164 @@ class VideoModule(VideoFields, XModule):
|
||||
'start': self.start_time.total_seconds(),
|
||||
'sub': self.sub,
|
||||
'track': track_url,
|
||||
'youtube_streams': _create_youtube_string(self),
|
||||
'youtube_streams': create_youtube_string(self),
|
||||
# TODO: Later on the value 1500 should be taken from some global
|
||||
# configuration setting field.
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_test_url': settings.YOUTUBE_TEST_URL,
|
||||
'transcript_language': transcript_language,
|
||||
'transcript_languages': json.dumps(transcript_languages),
|
||||
'transcript_translation_url': self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/translation',
|
||||
'transcript_available_translations_url': self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/available_translations',
|
||||
})
|
||||
|
||||
def get_transcript(self, subs_id):
|
||||
'''
|
||||
def get_transcript(self):
|
||||
"""
|
||||
Returns transcript in *.srt format.
|
||||
|
||||
Args:
|
||||
`subs_id`: str, subtitles id
|
||||
|
||||
Raises:
|
||||
- NotFoundError if cannot find transcript file in storage.
|
||||
- ValueError if transcript file is empty or incorrect JSON.
|
||||
- KeyError if transcript file has incorrect format.
|
||||
'''
|
||||
|
||||
filename = 'subs_{0}.srt.sjson'.format(subs_id)
|
||||
content_location = StaticContent.compute_location(
|
||||
self.location.org, self.location.course, filename
|
||||
)
|
||||
sjson_transcripts = contentstore().find(content_location)
|
||||
str_subs = _generate_srt_from_sjson(json.loads(sjson_transcripts.data), speed=1.0)
|
||||
"""
|
||||
lang = self.transcript_language
|
||||
subs_id = self.sub if lang == 'en' else self.youtube_id_1_0
|
||||
data = asset(self.location, subs_id, lang).data
|
||||
str_subs = generate_srt_from_sjson(json.loads(data), speed=1.0)
|
||||
if not str_subs:
|
||||
log.debug('generate_srt_from_sjson produces no subtitles')
|
||||
raise ValueError
|
||||
|
||||
return str_subs
|
||||
|
||||
|
||||
@XBlock.handler
|
||||
def download_transcript(self, __, ___):
|
||||
def transcript(self, request, dispatch):
|
||||
"""
|
||||
This is called to get transcript file without timecodes to student.
|
||||
"""
|
||||
try:
|
||||
subs = self.get_transcript(self.sub)
|
||||
except (NotFoundError):
|
||||
log.debug("Can't find content in storage for %s transcript", self.sub)
|
||||
return Response(status=404)
|
||||
except (ValueError, KeyError):
|
||||
log.debug("Invalid transcript JSON.")
|
||||
return Response(status=400)
|
||||
Entry point for transcript handlers.
|
||||
|
||||
response = Response(
|
||||
subs,
|
||||
headerlist=[
|
||||
('Content-Disposition', 'attachment; filename="{0}.srt"'.format(self.sub)),
|
||||
])
|
||||
response.content_type="application/x-subrip"
|
||||
Request GET should contains 2-char language code for `download`
|
||||
and additionally `videoId` for `translation`.
|
||||
|
||||
Dispatches:
|
||||
`download`: returns SRT file.
|
||||
`translation`: returns jsoned translation text.
|
||||
`available_translations`: returns list of languages, for which SRT files exist. For 'en' check if SJSON exists.
|
||||
"""
|
||||
if dispatch == 'translation':
|
||||
if 'language' not in request.GET or 'videoId' not in request.GET:
|
||||
log.info("Invalid /transcript GET parameters.")
|
||||
return Response(status=400)
|
||||
|
||||
lang = request.GET.get('language')
|
||||
if lang not in ['en'] + self.transcripts.keys():
|
||||
log.info("Video: transcript facilities are not available for given language.")
|
||||
return Response(status=404)
|
||||
if lang != self.transcript_language:
|
||||
self.transcript_language = lang
|
||||
|
||||
try:
|
||||
transcript = self.translation(request.GET.get('videoId'))
|
||||
except TranscriptException as ex:
|
||||
log.info(ex.message)
|
||||
response = Response(status=404)
|
||||
else:
|
||||
response = Response(transcript)
|
||||
response.content_type = 'application/json'
|
||||
|
||||
elif dispatch == 'download':
|
||||
try:
|
||||
subs = self.get_transcript()
|
||||
except (NotFoundError, ValueError, KeyError):
|
||||
log.debug("Video@download exception")
|
||||
response = Response(status=404)
|
||||
else:
|
||||
response = Response(
|
||||
subs,
|
||||
headerlist=[
|
||||
('Content-Disposition', 'attachment; filename="{0}.srt"'.format(self.transcript_language)),
|
||||
]
|
||||
)
|
||||
response.content_type = "application/x-subrip"
|
||||
|
||||
elif dispatch == 'available_translations':
|
||||
available_translations = []
|
||||
if self.sub: # check if sjson exists for 'en'.
|
||||
try:
|
||||
asset(self.location, self.sub, 'en')
|
||||
except NotFoundError:
|
||||
passs
|
||||
else:
|
||||
available_translations = ['en']
|
||||
for lang in self.transcripts:
|
||||
try:
|
||||
asset(self.location, None, None, self.transcripts[lang])
|
||||
except NotFoundError:
|
||||
continue
|
||||
available_translations.append(lang)
|
||||
if available_translations:
|
||||
response = Response(json.dumps(available_translations))
|
||||
response.content_type = 'application/json'
|
||||
else:
|
||||
response = Response(status=404)
|
||||
else: # unknown dispatch
|
||||
log.debug("Dispatch is not allowed")
|
||||
response = Response(status=404)
|
||||
|
||||
return response
|
||||
|
||||
def translation(self, subs_id):
|
||||
"""
|
||||
This is called to get transcript file for specific language.
|
||||
|
||||
subs_id: str: must be on of: self.sub or one of youtube_ids.
|
||||
|
||||
Logic flow:
|
||||
|
||||
If english -> give back `sub` subtitles:
|
||||
Return what we have in contentstore for given subs_id,
|
||||
We should not regenerate needed transcripts, if, for example, they present for youtube 1.0 speed,
|
||||
and we need for other speeds. Such generation should be done in transcripts workflow.
|
||||
If non-english:
|
||||
a) extract subs_id from srt file name
|
||||
if non-youtube:
|
||||
b) try to find sjson by subs_id and return if sucessful
|
||||
c) otherwise generate sjson from srt and return it.
|
||||
if youtube:
|
||||
b) try to find sjson by subs_id and return if sucessful
|
||||
c) generate sjson from srt for all youtube speeds
|
||||
|
||||
Filenames naming:
|
||||
en: subs_videoid.srt.sjson
|
||||
non_en: uk_subs_videoid.srt.sjson
|
||||
"""
|
||||
if self.transcript_language == 'en':
|
||||
return asset(self.location, subs_id).data
|
||||
|
||||
if not self.youtube_id_1_0: # Non-youtube (HTML5) case:
|
||||
return get_or_create_sjson(self)
|
||||
|
||||
# Youtube case:
|
||||
youtube_ids = youtube_speed_dict(self)
|
||||
assert subs_id in youtube_ids
|
||||
|
||||
try:
|
||||
sjson_transcript = asset(self.location, subs_id, self.transcript_language).data
|
||||
except (NotFoundError):
|
||||
log.info("Can't find content in storage for %s transcript: generating.", subs_id)
|
||||
generate_sjson_for_all_speeds(
|
||||
self,
|
||||
self.transcripts[self.transcript_language],
|
||||
{speed: subs_id for subs_id, speed in youtube_ids.iteritems()},
|
||||
self.transcript_language
|
||||
)
|
||||
sjson_transcript = asset(self.location, subs_id, self.transcript_language).data
|
||||
return sjson_transcript
|
||||
|
||||
|
||||
class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor):
|
||||
"""Descriptor for `VideoModule`."""
|
||||
module_class = VideoModule
|
||||
download_transcript = module_attr('download_transcript')
|
||||
transcript = module_attr('transcript')
|
||||
|
||||
tabs = [
|
||||
{
|
||||
@@ -317,7 +453,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
'''
|
||||
"""
|
||||
Mostly handles backward compatibility issues.
|
||||
|
||||
`source` is deprecated field.
|
||||
@@ -327,7 +463,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
|
||||
b) If `source` is cleared it is not shown anymore.
|
||||
c) If `source` exists and `source` in `html5_sources`, do not show `source`
|
||||
field. `download_video` field has value True.
|
||||
'''
|
||||
"""
|
||||
super(VideoDescriptor, self).__init__(*args, **kwargs)
|
||||
# For backwards compatibility -- if we've got XML data, parse
|
||||
# it out and set the metadata fields
|
||||
@@ -358,6 +494,13 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
|
||||
if not download_track['explicitly_set'] and self.track:
|
||||
self.download_track = True
|
||||
|
||||
def save_with_metadata(self, user):
|
||||
"""
|
||||
Save module with updated metadata to database."
|
||||
"""
|
||||
self.save()
|
||||
self.runtime.modulestore.update_item(self, user.id if user else None)
|
||||
|
||||
@property
|
||||
def editable_metadata_fields(self):
|
||||
editable_fields = super(VideoDescriptor, self).editable_metadata_fields
|
||||
@@ -408,7 +551,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
|
||||
Returns an xml string representing this module.
|
||||
"""
|
||||
xml = etree.Element('video')
|
||||
youtube_string = _create_youtube_string(self)
|
||||
youtube_string = create_youtube_string(self)
|
||||
# Mild workaround to ensure that tests pass -- if a field
|
||||
# is set to its default value, we don't need to write it out.
|
||||
if youtube_string and youtube_string != '1.00:OEoXaMPEzfM':
|
||||
@@ -440,11 +583,18 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
|
||||
ele.set('src', self.track)
|
||||
xml.append(ele)
|
||||
|
||||
# sorting for easy testing of resulting xml
|
||||
for transcript_language in sorted(self.transcripts.keys()):
|
||||
ele = etree.Element('transcript')
|
||||
ele.set('language', transcript_language)
|
||||
ele.set('src', self.transcripts[transcript_language])
|
||||
xml.append(ele)
|
||||
|
||||
return xml
|
||||
|
||||
def get_context(self):
|
||||
"""
|
||||
Extend context by data for transcripts basic tab.
|
||||
Extend context by data for transcript basic tab.
|
||||
"""
|
||||
_context = super(VideoDescriptor, self).get_context()
|
||||
|
||||
@@ -503,7 +653,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
|
||||
youtube_id = deserialize_field(cls.youtube_id_1_0, pieces[1])
|
||||
ret[speed] = youtube_id
|
||||
except (ValueError, IndexError):
|
||||
log.warning('Invalid YouTube ID: %s' % video)
|
||||
log.warning('Invalid YouTube ID: %s', video)
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
@@ -527,7 +677,6 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
|
||||
'from': 'start_time',
|
||||
'to': 'end_time'
|
||||
}
|
||||
|
||||
sources = xml.findall('source')
|
||||
if sources:
|
||||
field_data['html5_sources'] = [ele.get('src') for ele in sources]
|
||||
@@ -536,6 +685,10 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
|
||||
if track is not None:
|
||||
field_data['track'] = track.get('src')
|
||||
|
||||
transcripts = xml.findall('transcript')
|
||||
if transcripts:
|
||||
field_data['transcripts'] = {tr.get('language'): tr.get('src') for tr in transcripts}
|
||||
|
||||
for attr, value in xml.items():
|
||||
if attr in compat_keys:
|
||||
attr = compat_keys[attr]
|
||||
@@ -572,80 +725,3 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
|
||||
field_data['download_track'] = True
|
||||
|
||||
return field_data
|
||||
|
||||
|
||||
def _create_youtube_string(module):
|
||||
"""
|
||||
Create a string of Youtube IDs from `module`'s metadata
|
||||
attributes. Only writes a speed if an ID is present in the
|
||||
module. Necessary for backwards compatibility with XML-based
|
||||
courses.
|
||||
"""
|
||||
youtube_ids = [
|
||||
module.youtube_id_0_75,
|
||||
module.youtube_id_1_0,
|
||||
module.youtube_id_1_25,
|
||||
module.youtube_id_1_5
|
||||
]
|
||||
youtube_speeds = ['0.75', '1.00', '1.25', '1.50']
|
||||
return ','.join([':'.join(pair)
|
||||
for pair
|
||||
in zip(youtube_speeds, youtube_ids)
|
||||
if pair[1]])
|
||||
|
||||
|
||||
def _generate_subs(speed, source_speed, source_subs):
|
||||
"""
|
||||
Generate transcripts from one speed to another speed.
|
||||
|
||||
Args:
|
||||
`speed`: float, for this speed subtitles will be generated,
|
||||
`source_speed`: float, speed of source_subs
|
||||
`soource_subs`: dict, existing subtitles for speed `source_speed`.
|
||||
|
||||
Returns:
|
||||
`subs`: dict, actual subtitles.
|
||||
"""
|
||||
if speed == source_speed:
|
||||
return source_subs
|
||||
|
||||
coefficient = 1.0 * speed / source_speed
|
||||
subs = {
|
||||
'start': [
|
||||
int(round(timestamp * coefficient)) for
|
||||
timestamp in source_subs['start']
|
||||
],
|
||||
'end': [
|
||||
int(round(timestamp * coefficient)) for
|
||||
timestamp in source_subs['end']
|
||||
],
|
||||
'text': source_subs['text']}
|
||||
return subs
|
||||
|
||||
|
||||
def _generate_srt_from_sjson(sjson_subs, speed):
|
||||
"""Generate transcripts with speed = 1.0 from sjson to SubRip (*.srt).
|
||||
|
||||
:param sjson_subs: "sjson" subs.
|
||||
:param speed: speed of `sjson_subs`.
|
||||
:returns: "srt" subs.
|
||||
"""
|
||||
|
||||
output = ''
|
||||
|
||||
equal_len = len(sjson_subs['start']) == len(sjson_subs['end']) == len(sjson_subs['text'])
|
||||
if not equal_len:
|
||||
return output
|
||||
|
||||
sjson_speed_1 = _generate_subs(speed, 1, sjson_subs)
|
||||
|
||||
for i in range(len(sjson_speed_1['start'])):
|
||||
item = SubRipItem(
|
||||
index=i,
|
||||
start=SubRipTime(milliseconds=sjson_speed_1['start'][i]),
|
||||
end=SubRipTime(milliseconds=sjson_speed_1['end'][i]),
|
||||
text=sjson_speed_1['text'][i]
|
||||
)
|
||||
output += (unicode(item))
|
||||
output += '\n'
|
||||
return output
|
||||
25
common/lib/xmodule/xmodule/video_module/video_utils.py
Normal file
25
common/lib/xmodule/xmodule/video_module/video_utils.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Module containts utils specific for video_module but not for transcripts.
|
||||
"""
|
||||
|
||||
|
||||
def create_youtube_string(module):
|
||||
"""
|
||||
Create a string of Youtube IDs from `module`'s metadata
|
||||
attributes. Only writes a speed if an ID is present in the
|
||||
module. Necessary for backwards compatibility with XML-based
|
||||
courses.
|
||||
"""
|
||||
youtube_ids = [
|
||||
module.youtube_id_0_75,
|
||||
module.youtube_id_1_0,
|
||||
module.youtube_id_1_25,
|
||||
module.youtube_id_1_5
|
||||
]
|
||||
youtube_speeds = ['0.75', '1.00', '1.25', '1.50']
|
||||
return ','.join([
|
||||
':'.join(pair)
|
||||
for pair
|
||||
in zip(youtube_speeds, youtube_ids)
|
||||
if pair[1]
|
||||
])
|
||||
17
common/test/data/uploads/zh_subs_OEoXaMPEzfM.srt.sjson
Normal file
17
common/test/data/uploads/zh_subs_OEoXaMPEzfM.srt.sjson
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"start": [
|
||||
270,
|
||||
2720,
|
||||
5430
|
||||
],
|
||||
"end": [
|
||||
2720,
|
||||
5430,
|
||||
7160
|
||||
],
|
||||
"text": [
|
||||
"好 各位同学",
|
||||
"我们今天要讲的题目是",
|
||||
"从算筹到ENIAC"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
@shard_2
|
||||
Feature: LMS.Video component
|
||||
As a student, I want to view course videos in LMS.
|
||||
Feature: LMS Video component
|
||||
As a student, I want to view course videos in LMS
|
||||
|
||||
# 0
|
||||
Scenario: Video component stores position correctly when page is reloaded
|
||||
@@ -58,7 +58,7 @@ Feature: LMS.Video component
|
||||
And error message has correct text
|
||||
|
||||
# 8
|
||||
Scenario: Video component stores speed correctly when each video is in separate sequence.
|
||||
Scenario: Video component stores speed correctly when each video is in separate sequence
|
||||
Given I am registered for the course "test_course"
|
||||
And it has a video "A" in "Youtube" mode in position "1" of sequential
|
||||
And a video "B" in "Youtube" mode in position "2" of sequential
|
||||
@@ -78,3 +78,15 @@ Feature: LMS.Video component
|
||||
Then video "B" should start playing at speed "0.50"
|
||||
When I open video "C"
|
||||
Then video "C" should start playing at speed "1.0"
|
||||
|
||||
# 9
|
||||
Scenario: Language menu in Video component works correctly
|
||||
Given the course has a Video component in Youtube mode:
|
||||
| transcripts | sub |
|
||||
| {"zh": "OEoXaMPEzfM"} | OEoXaMPEzfM |
|
||||
And I make sure captions are closed
|
||||
And I see video menu "language" with correct items
|
||||
And I select language with code "zh"
|
||||
Then I see "好 各位同学" text in the captions
|
||||
And I select language with code "en"
|
||||
And I see "Hi, welcome to Edx." text in the captions
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#pylint: disable=C0111
|
||||
|
||||
from lettuce import world, step
|
||||
import json
|
||||
from common import i_am_registered_for_the_course, section_location, visit_scenario_item
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
import os
|
||||
from functools import partial
|
||||
from xmodule.contentstore.django import contentstore
|
||||
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
|
||||
LANGUAGES = settings.ALL_LANGUAGES
|
||||
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
@@ -14,6 +25,16 @@ HTML5_SOURCES = [
|
||||
HTML5_SOURCES_INCORRECT = [
|
||||
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp99',
|
||||
]
|
||||
VIDEO_BUTTONS = {
|
||||
'CC': '.hide-subtitles',
|
||||
'volume': '.volume',
|
||||
'play': '.video_control.play',
|
||||
'pause': '.video_control.pause',
|
||||
}
|
||||
VIDEO_MENUS = {
|
||||
'language': '.lang .menu',
|
||||
'speed': '.speed .menu',
|
||||
}
|
||||
|
||||
VIDEO_BUTTONS = {
|
||||
'CC': '.hide-subtitles',
|
||||
@@ -22,9 +43,6 @@ VIDEO_BUTTONS = {
|
||||
'pause': '.video_control.pause',
|
||||
}
|
||||
|
||||
# We should wait 300 ms for event handler invocation + 200ms for safety.
|
||||
DELAY = 0.5
|
||||
|
||||
coursenum = 'test_course'
|
||||
sequence = {}
|
||||
|
||||
@@ -33,20 +51,20 @@ def does_not_autoplay(_step, video_type):
|
||||
assert(world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False')
|
||||
|
||||
|
||||
@step('the course has a Video component in (.*) mode$')
|
||||
@step('the course has a Video component in (.*) mode(?:\:)?$')
|
||||
def view_video(_step, player_mode):
|
||||
|
||||
i_am_registered_for_the_course(_step, coursenum)
|
||||
|
||||
# Make sure we have a video
|
||||
add_video_to_course(coursenum, player_mode.lower())
|
||||
add_video_to_course(coursenum, player_mode.lower(), _step.hashes)
|
||||
visit_scenario_item('SECTION')
|
||||
|
||||
|
||||
@step('a video "([^"]*)" in "([^"]*)" mode in position "([^"]*)" of sequential$')
|
||||
@step('a video "([^"]*)" in "([^"]*)" mode in position "([^"]*)" of sequential(?:\:)?$')
|
||||
def add_video(_step, player_id, player_mode, position):
|
||||
sequence[player_id] = position
|
||||
add_video_to_course(coursenum, player_mode.lower(), display_name=player_id)
|
||||
add_video_to_course(coursenum, player_mode.lower(), _step.hashes, display_name=player_id)
|
||||
|
||||
|
||||
@step('I open the section with videos$')
|
||||
@@ -70,49 +88,55 @@ def check_video_speed(_step, player_id, speed):
|
||||
speed_css = '.speeds p.active'
|
||||
assert world.css_has_text(speed_css, '{0}x'.format(speed))
|
||||
|
||||
def add_video_to_course(course, player_mode, display_name='Video'):
|
||||
|
||||
def add_video_to_course(course, player_mode, hashes, display_name='Video'):
|
||||
category = 'video'
|
||||
|
||||
kwargs = {
|
||||
'parent_location': section_location(course),
|
||||
'category': category,
|
||||
'display_name': display_name
|
||||
'display_name': display_name,
|
||||
'metadata': {},
|
||||
}
|
||||
|
||||
if player_mode == 'html5':
|
||||
kwargs.update({
|
||||
'metadata': {
|
||||
'youtube_id_1_0': '',
|
||||
'youtube_id_0_75': '',
|
||||
'youtube_id_1_25': '',
|
||||
'youtube_id_1_5': '',
|
||||
'html5_sources': HTML5_SOURCES
|
||||
}
|
||||
kwargs['metadata'].update({
|
||||
'youtube_id_1_0': '',
|
||||
'youtube_id_0_75': '',
|
||||
'youtube_id_1_25': '',
|
||||
'youtube_id_1_5': '',
|
||||
'html5_sources': HTML5_SOURCES
|
||||
})
|
||||
if player_mode == 'youtube_html5':
|
||||
kwargs.update({
|
||||
'metadata': {
|
||||
'html5_sources': HTML5_SOURCES
|
||||
}
|
||||
kwargs['metadata'].update({
|
||||
'html5_sources': HTML5_SOURCES
|
||||
})
|
||||
if player_mode == 'youtube_html5_unsupported_video':
|
||||
kwargs.update({
|
||||
'metadata': {
|
||||
'html5_sources': HTML5_SOURCES_INCORRECT
|
||||
}
|
||||
kwargs['metadata'].update({
|
||||
'html5_sources': HTML5_SOURCES_INCORRECT
|
||||
})
|
||||
if player_mode == 'html5_unsupported_video':
|
||||
kwargs.update({
|
||||
'metadata': {
|
||||
'youtube_id_1_0': '',
|
||||
'youtube_id_0_75': '',
|
||||
'youtube_id_1_25': '',
|
||||
'youtube_id_1_5': '',
|
||||
'html5_sources': HTML5_SOURCES_INCORRECT
|
||||
}
|
||||
kwargs['metadata'].update({
|
||||
'youtube_id_1_0': '',
|
||||
'youtube_id_0_75': '',
|
||||
'youtube_id_1_25': '',
|
||||
'youtube_id_1_5': '',
|
||||
'html5_sources': HTML5_SOURCES_INCORRECT
|
||||
})
|
||||
|
||||
world.ItemFactory.create(**kwargs)
|
||||
if hashes:
|
||||
kwargs['metadata'].update(hashes[0])
|
||||
|
||||
if 'transcripts' in kwargs['metadata']:
|
||||
kwargs['metadata']['transcripts'] = json.loads(kwargs['metadata']['transcripts'])
|
||||
|
||||
if 'sub' in kwargs['metadata']:
|
||||
_upload_file(kwargs['metadata']['sub'], 'en', world.scenario_dict['COURSE'].location)
|
||||
|
||||
for lang, videoId in kwargs['metadata']['transcripts'].items():
|
||||
_upload_file(videoId, lang, world.scenario_dict['COURSE'].location)
|
||||
|
||||
world.scenario_dict['VIDEO'] = world.ItemFactory.create(**kwargs)
|
||||
|
||||
|
||||
@step('youtube server is up and response time is (.*) seconds$')
|
||||
@@ -152,6 +176,92 @@ def error_message_has_correct_text(_step):
|
||||
assert world.css_has_text(selector, text)
|
||||
|
||||
|
||||
@step('I make sure captions are (.+)$')
|
||||
def set_captions_visibility_state(_step, captions_state):
|
||||
SELECTOR = '.closed .subtitles'
|
||||
if world.is_css_not_present(SELECTOR):
|
||||
if captions_state == 'closed':
|
||||
world.css_find('.hide-subtitles').click()
|
||||
else:
|
||||
if captions_state != 'closed':
|
||||
world.css_find('.hide-subtitles').click()
|
||||
|
||||
|
||||
@step('I see video menu "([^"]*)" with correct items$')
|
||||
def i_see_menu(_step, menu):
|
||||
_open_menu(menu)
|
||||
menu_items = world.css_find(VIDEO_MENUS[menu] + ' li')
|
||||
Video = world.scenario_dict['VIDEO']
|
||||
transcripts = dict(Video.transcripts)
|
||||
if Video.sub:
|
||||
transcripts.update({
|
||||
'en': Video.sub
|
||||
})
|
||||
|
||||
languages = {i[0]: i[1] for i in LANGUAGES}
|
||||
transcripts = {k: languages[k] for k in transcripts}
|
||||
|
||||
for code, label in transcripts.items():
|
||||
assert any([i.text == label for i in menu_items])
|
||||
assert any([i['data-lang-code'] == code for i in menu_items])
|
||||
|
||||
|
||||
@step('I see "([^"]*)" text in the captions$')
|
||||
def check_text_in_the_captions(_step, text):
|
||||
assert world.browser.is_text_present(text.strip())
|
||||
|
||||
|
||||
@step('I select language with code "([^"]*)"$')
|
||||
def select_language(_step, code):
|
||||
_open_menu("language")
|
||||
selector = VIDEO_MENUS["language"] + ' li[data-lang-code={code}]'.format(
|
||||
code=code
|
||||
)
|
||||
item = world.css_find(selector)
|
||||
|
||||
item.click()
|
||||
|
||||
assert world.css_has_class(selector, 'active')
|
||||
assert len(world.css_find(VIDEO_MENUS["language"] + ' li.active')) == 1
|
||||
assert world.css_visible('.subtitles')
|
||||
world.wait_for_ajax_complete()
|
||||
|
||||
|
||||
@step('I click on video button "([^"]*)"$')
|
||||
def click_button(_step, button):
|
||||
world.css_find(VIDEO_BUTTONS[button]).click()
|
||||
|
||||
|
||||
def _upload_file(videoId, lang, location):
|
||||
if lang == 'en':
|
||||
filename = 'subs_{0}.srt.sjson'.format(videoId)
|
||||
else:
|
||||
filename = '{0}_subs_{1}.srt.sjson'.format(lang, videoId)
|
||||
|
||||
path = os.path.join(TEST_ROOT, 'uploads/', filename)
|
||||
f = open(os.path.abspath(path))
|
||||
mime_type = "application/json"
|
||||
|
||||
content_location = StaticContent.compute_location(
|
||||
location.org, location.course, filename
|
||||
)
|
||||
|
||||
sc_partial = partial(StaticContent, content_location, filename, mime_type)
|
||||
content = sc_partial(f.read())
|
||||
|
||||
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(
|
||||
content,
|
||||
tempfile_path=None
|
||||
)
|
||||
del_cached_content(thumbnail_location)
|
||||
|
||||
if thumbnail_content is not None:
|
||||
content.thumbnail_location = thumbnail_location
|
||||
|
||||
contentstore().save(content)
|
||||
del_cached_content(content.location)
|
||||
|
||||
|
||||
def _navigate_to_an_item_in_a_sequence(number):
|
||||
sequence_css = 'a[data-element="{0}"]'.format(number)
|
||||
world.css_click(sequence_css)
|
||||
@@ -165,7 +275,6 @@ def _change_video_speed(speed):
|
||||
|
||||
@step('I click video button "([^"]*)"$')
|
||||
def click_button_video(_step, button_type):
|
||||
world.wait(DELAY)
|
||||
world.wait_for_ajax_complete()
|
||||
button = button_type.strip()
|
||||
world.css_click(VIDEO_BUTTONS[button])
|
||||
@@ -184,3 +293,9 @@ def seek_video_to_n_seconds(_step, seconds):
|
||||
time = float(seconds.strip())
|
||||
jsCode = "$('.video').data('video-player-state').videoPlayer.onSlideSeek({{time: {0:f}}})".format(time)
|
||||
world.browser.execute_script(jsCode)
|
||||
|
||||
|
||||
def _open_menu(menu):
|
||||
world.browser.execute_script("$('{selector}').parent().addClass('open')".format(
|
||||
selector=VIDEO_MENUS[menu]
|
||||
))
|
||||
|
||||
@@ -90,9 +90,11 @@ class BaseTestXmodule(ModuleStoreTestCase):
|
||||
self.item_descriptor._field_data = LmsFieldData(self.item_descriptor._field_data, student_data)
|
||||
|
||||
self.item_descriptor.xmodule_runtime = self.new_module_runtime()
|
||||
self.item_module = self.item_descriptor
|
||||
|
||||
self.item_url = Location(self.item_module.location).url()
|
||||
#self.item_module = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
#self.item_module is None at this time
|
||||
|
||||
self.item_url = Location(self.item_descriptor.location).url()
|
||||
|
||||
def setup_course(self):
|
||||
self.course = CourseFactory.create(data=self.COURSE_DATA)
|
||||
@@ -130,7 +132,7 @@ class BaseTestXmodule(ModuleStoreTestCase):
|
||||
self.assertTrue(all(self.login_statuses))
|
||||
|
||||
def setUp(self):
|
||||
self.setup_course();
|
||||
self.setup_course()
|
||||
self.initialize_module(metadata=self.METADATA, data=self.DATA)
|
||||
|
||||
def get_url(self, dispatch):
|
||||
|
||||
@@ -27,8 +27,8 @@ class TestLTI(BaseTestXmodule):
|
||||
mocked_signature_after_sign = u'my_signature%3D'
|
||||
mocked_decoded_signature = u'my_signature='
|
||||
|
||||
lti_id = self.item_module.lti_id
|
||||
module_id = unicode(urllib.quote(self.item_module.id))
|
||||
lti_id = self.item_descriptor.lti_id
|
||||
module_id = unicode(urllib.quote(self.item_descriptor.id))
|
||||
user_id = unicode(self.item_descriptor.xmodule_runtime.anonymous_student_id)
|
||||
|
||||
sourcedId = "{id}:{resource_link}:{user_id}".format(
|
||||
@@ -38,9 +38,9 @@ class TestLTI(BaseTestXmodule):
|
||||
)
|
||||
|
||||
lis_outcome_service_url = 'https://{host}{path}'.format(
|
||||
host=self.item_descriptor.xmodule_runtime.hostname,
|
||||
path=self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'grade_handler', thirdparty=True).rstrip('/?')
|
||||
)
|
||||
host=self.item_descriptor.xmodule_runtime.hostname,
|
||||
path=self.item_descriptor.xmodule_runtime.handler_url(self.item_descriptor, 'grade_handler', thirdparty=True).rstrip('/?')
|
||||
)
|
||||
self.correct_headers = {
|
||||
u'user_id': user_id,
|
||||
u'oauth_callback': u'about:blank',
|
||||
@@ -63,13 +63,13 @@ class TestLTI(BaseTestXmodule):
|
||||
saved_sign = oauthlib.oauth1.Client.sign
|
||||
|
||||
self.expected_context = {
|
||||
'display_name': self.item_module.display_name,
|
||||
'display_name': self.item_descriptor.display_name,
|
||||
'input_fields': self.correct_headers,
|
||||
'element_class': self.item_module.category,
|
||||
'element_id': self.item_module.location.html_id(),
|
||||
'element_class': self.item_descriptor.category,
|
||||
'element_id': self.item_descriptor.location.html_id(),
|
||||
'launch_url': 'http://www.example.com', # default value
|
||||
'open_in_a_new_page': True,
|
||||
'form_url': self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'preview_handler').rstrip('/?'),
|
||||
'form_url': self.item_descriptor.xmodule_runtime.handler_url(self.item_descriptor, 'preview_handler').rstrip('/?'),
|
||||
}
|
||||
|
||||
def mocked_sign(self, *args, **kwargs):
|
||||
@@ -92,11 +92,11 @@ class TestLTI(BaseTestXmodule):
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def test_lti_constructor(self):
|
||||
generated_content = self.item_module.render('student_view').content
|
||||
generated_content = self.item_descriptor.render('student_view').content
|
||||
expected_content = self.runtime.render_template('lti.html', self.expected_context)
|
||||
self.assertEqual(generated_content, expected_content)
|
||||
|
||||
def test_lti_preview_handler(self):
|
||||
generated_content = self.item_module.preview_handler(None, None).body
|
||||
generated_content = self.item_descriptor.preview_handler(None, None).body
|
||||
expected_content = self.runtime.render_template('lti_form.html', self.expected_context)
|
||||
self.assertEqual(generated_content, expected_content)
|
||||
|
||||
351
lms/djangoapps/courseware/tests/test_video_handlers.py
Normal file
351
lms/djangoapps/courseware/tests/test_video_handlers.py
Normal file
@@ -0,0 +1,351 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Video xmodule tests in mongo."""
|
||||
|
||||
from mock import patch
|
||||
import os
|
||||
import tempfile
|
||||
import textwrap
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from webob import Request
|
||||
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from . import BaseTestXmodule
|
||||
from .test_video_xml import SOURCE_XML
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from xmodule.exceptions import NotFoundError
|
||||
|
||||
|
||||
def _create_srt_file(content=None):
|
||||
"""
|
||||
Create srt file in filesystem.
|
||||
"""
|
||||
content = content or textwrap.dedent("""
|
||||
0
|
||||
00:00:00,12 --> 00:00:00,100
|
||||
Привіт, edX вітає вас.
|
||||
""")
|
||||
srt_file = tempfile.NamedTemporaryFile(suffix=".srt")
|
||||
srt_file.content_type = 'application/x-subrip'
|
||||
srt_file.write(content)
|
||||
srt_file.seek(0)
|
||||
return srt_file
|
||||
|
||||
|
||||
def _clear_assets(location):
|
||||
"""
|
||||
Clear all assets for location.
|
||||
"""
|
||||
store = contentstore()
|
||||
|
||||
content_location = StaticContent.compute_location(
|
||||
location.org, location.course, location.name
|
||||
)
|
||||
|
||||
assets, __ = store.get_all_content_for_course(content_location)
|
||||
for asset in assets:
|
||||
asset_location = Location(asset["_id"])
|
||||
del_cached_content(asset_location)
|
||||
id = StaticContent.get_id_from_location(asset_location)
|
||||
store.delete(id)
|
||||
|
||||
|
||||
def _get_subs_id(filename):
|
||||
basename = os.path.splitext(os.path.basename(filename))[0]
|
||||
return basename.replace('subs_', '').replace('.srt', '')
|
||||
|
||||
|
||||
def _create_file(content=''):
|
||||
"""
|
||||
Create temporary subs_somevalue.srt.sjson file.
|
||||
"""
|
||||
sjson_file = tempfile.NamedTemporaryFile(prefix="subs_", suffix=".srt.sjson")
|
||||
sjson_file.content_type = 'application/json'
|
||||
sjson_file.write(textwrap.dedent(content))
|
||||
sjson_file.seek(0)
|
||||
return sjson_file
|
||||
|
||||
|
||||
def _upload_sjson_file(subs_file, location, default_filename='subs_{}.srt.sjson'):
|
||||
filename = default_filename.format(_get_subs_id(subs_file.name))
|
||||
_upload_file(subs_file, location, filename)
|
||||
|
||||
|
||||
def _upload_file(subs_file, location, filename):
|
||||
mime_type = subs_file.content_type
|
||||
content_location = StaticContent.compute_location(
|
||||
location.org, location.course, filename
|
||||
)
|
||||
content = StaticContent(content_location, filename, mime_type, subs_file.read())
|
||||
contentstore().save(content)
|
||||
del_cached_content(content.location)
|
||||
|
||||
|
||||
class TestVideo(BaseTestXmodule):
|
||||
"""Integration tests: web client + mongo."""
|
||||
CATEGORY = "video"
|
||||
DATA = SOURCE_XML
|
||||
METADATA = {}
|
||||
|
||||
def test_handle_ajax_wrong_dispatch(self):
|
||||
responses = {
|
||||
user.username: self.clients[user.username].post(
|
||||
self.get_url('whatever'),
|
||||
{},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
for user in self.users
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
set([
|
||||
response.status_code
|
||||
for _, response in responses.items()
|
||||
]).pop(),
|
||||
404)
|
||||
|
||||
def test_handle_ajax(self):
|
||||
|
||||
data = [
|
||||
{'speed': 2.0},
|
||||
{'saved_video_position': "00:00:10"},
|
||||
{'transcript_language': json.dumps('uk')},
|
||||
]
|
||||
for sample in data:
|
||||
response = self.clients[self.users[0].username].post(
|
||||
self.get_url('save_user_state'),
|
||||
sample,
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertEqual(self.item_descriptor.speed, None)
|
||||
self.item_descriptor.handle_ajax('save_user_state', {'speed': json.dumps(2.0)})
|
||||
self.assertEqual(self.item_descriptor.speed, 2.0)
|
||||
self.assertEqual(self.item_descriptor.global_speed, 2.0)
|
||||
|
||||
self.assertEqual(self.item_descriptor.saved_video_position, timedelta(0))
|
||||
self.item_descriptor.handle_ajax('save_user_state', {'saved_video_position': "00:00:10"})
|
||||
self.assertEqual(self.item_descriptor.saved_video_position, timedelta(0, 10))
|
||||
|
||||
self.assertEqual(self.item_descriptor.transcript_language, 'en')
|
||||
self.item_descriptor.handle_ajax('save_user_state', {'transcript_language': json.dumps("uk")})
|
||||
self.assertEqual(self.item_descriptor.transcript_language, 'uk')
|
||||
|
||||
def tearDown(self):
|
||||
_clear_assets(self.item_descriptor.location)
|
||||
|
||||
|
||||
class TestVideoTranscriptTranslation(TestVideo):
|
||||
"""
|
||||
Test video handlers that provide translation transcripts.
|
||||
"""
|
||||
|
||||
non_en_file = _create_srt_file()
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
<transcript language="uk" src="{}"/>
|
||||
</video>
|
||||
""".format(os.path.split(non_en_file.name)[1])
|
||||
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestVideoTranscriptTranslation, self).setUp()
|
||||
self.item_descriptor.render('student_view')
|
||||
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
def test_language_is_not_supported(self):
|
||||
request = Request.blank('/download?language=ru')
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
|
||||
def test_download_transcript_not_exist(self):
|
||||
request = Request.blank('/download?language=en')
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
|
||||
@patch('xmodule.video_module.VideoModule.get_transcript', return_value='Subs!')
|
||||
def test_download_exist(self, __):
|
||||
request = Request.blank('/download?language=en')
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
self.assertEqual(response.body, 'Subs!')
|
||||
|
||||
def test_translation_fails(self):
|
||||
# No videoId
|
||||
request = Request.blank('/translation?language=ru')
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
self.assertEqual(response.status, '400 Bad Request')
|
||||
|
||||
# Language is not in available languages
|
||||
request = Request.blank('/translation?language=ru&videoId=12345')
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
|
||||
def test_translaton_en_success(self):
|
||||
subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]}
|
||||
good_sjson = _create_file(json.dumps(subs))
|
||||
_upload_sjson_file(good_sjson, self.item_descriptor.location)
|
||||
subs_id = _get_subs_id(good_sjson.name)
|
||||
|
||||
self.item.sub = subs_id
|
||||
request = Request.blank('/translation?language=en&videoId={}'.format(subs_id))
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
self.assertDictEqual(json.loads(response.body), subs)
|
||||
|
||||
def test_translaton_non_en_non_youtube_success(self):
|
||||
subs = {
|
||||
u'end': [100],
|
||||
u'start': [12],
|
||||
u'text': [
|
||||
u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.'
|
||||
]
|
||||
}
|
||||
self.non_en_file.seek(0)
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
|
||||
subs_id = _get_subs_id(self.non_en_file.name)
|
||||
|
||||
# manually clean youtube_id_1_0, as it has default value
|
||||
self.item.youtube_id_1_0 = ""
|
||||
request = Request.blank('/translation?language=uk&videoId={}'.format(subs_id))
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
self.assertDictEqual(json.loads(response.body), subs)
|
||||
|
||||
def test_translation_non_en_youtube(self):
|
||||
subs = {
|
||||
u'end': [100],
|
||||
u'start': [12],
|
||||
u'text': [
|
||||
u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.'
|
||||
]}
|
||||
self.non_en_file.seek(0)
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
|
||||
subs_id = _get_subs_id(self.non_en_file.name)
|
||||
|
||||
# youtube 1_0 request, will generate for all speeds for existing ids
|
||||
self.item.youtube_id_1_0 = subs_id
|
||||
self.item.youtube_id_0_75 = '0_75'
|
||||
request = Request.blank('/translation?language=uk&videoId={}'.format(subs_id))
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
self.assertDictEqual(json.loads(response.body), subs)
|
||||
|
||||
# 0_75 subs are exist
|
||||
request = Request.blank('/translation?language=uk&videoId={}'.format('0_75'))
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
calculated_0_75 = {
|
||||
u'end': [75],
|
||||
u'start': [9],
|
||||
u'text': [
|
||||
u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.'
|
||||
]
|
||||
}
|
||||
self.assertDictEqual(json.loads(response.body), calculated_0_75)
|
||||
# 1_5 will be generated from 1_0
|
||||
self.item.youtube_id_1_5 = '1_5'
|
||||
request = Request.blank('/translation?language=uk&videoId={}'.format('1_5'))
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
calculated_1_5 = {
|
||||
u'end': [150],
|
||||
u'start': [18],
|
||||
u'text': [
|
||||
u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.'
|
||||
]
|
||||
}
|
||||
self.assertDictEqual(json.loads(response.body), calculated_1_5)
|
||||
|
||||
|
||||
class TestVideoTranscriptsDownload(TestVideo):
|
||||
"""
|
||||
Make sure that `get_transcript` method works correctly
|
||||
"""
|
||||
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
</video>
|
||||
"""
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
}
|
||||
METADATA = {}
|
||||
|
||||
def setUp(self):
|
||||
super(TestVideoTranscriptsDownload, self).setUp()
|
||||
self.item_descriptor.render('student_view')
|
||||
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
def test_good_transcript(self):
|
||||
good_sjson = _create_file(content=textwrap.dedent("""\
|
||||
{
|
||||
"start": [
|
||||
270,
|
||||
2720
|
||||
],
|
||||
"end": [
|
||||
2720,
|
||||
5430
|
||||
],
|
||||
"text": [
|
||||
"Hi, welcome to Edx.",
|
||||
"Let's start with what is on your screen right now."
|
||||
]
|
||||
}
|
||||
"""))
|
||||
|
||||
_upload_sjson_file(good_sjson, self.item.location)
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
text = self.item.get_transcript()
|
||||
expected_text = textwrap.dedent("""\
|
||||
0
|
||||
00:00:00,270 --> 00:00:02,720
|
||||
Hi, welcome to Edx.
|
||||
|
||||
1
|
||||
00:00:02,720 --> 00:00:05,430
|
||||
Let's start with what is on your screen right now.
|
||||
|
||||
""")
|
||||
|
||||
self.assertEqual(text, expected_text)
|
||||
|
||||
def test_not_found_error(self):
|
||||
with self.assertRaises(NotFoundError):
|
||||
self.item.get_transcript()
|
||||
|
||||
def test_value_error(self):
|
||||
good_sjson = _create_file(content='bad content')
|
||||
|
||||
_upload_sjson_file(good_sjson, self.item.location)
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
self.item.get_transcript()
|
||||
|
||||
def test_key_error(self):
|
||||
good_sjson = _create_file(content="""
|
||||
{
|
||||
"start": [
|
||||
270,
|
||||
2720
|
||||
],
|
||||
"end": [
|
||||
2720,
|
||||
5430
|
||||
]
|
||||
}
|
||||
""")
|
||||
|
||||
_upload_sjson_file(good_sjson, self.item.location)
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
|
||||
with self.assertRaises(KeyError):
|
||||
self.item.get_transcript()
|
||||
@@ -1,46 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Video xmodule tests in mongo."""
|
||||
|
||||
from mock import patch, PropertyMock
|
||||
import os
|
||||
import tempfile
|
||||
import textwrap
|
||||
from functools import partial
|
||||
import json
|
||||
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from . import BaseTestXmodule
|
||||
from .test_video_xml import SOURCE_XML
|
||||
from .test_video_handlers import TestVideo
|
||||
from django.conf import settings
|
||||
from xmodule.video_module import _create_youtube_string
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from xmodule.exceptions import NotFoundError
|
||||
|
||||
class TestVideo(BaseTestXmodule):
|
||||
"""Integration tests: web client + mongo."""
|
||||
CATEGORY = "video"
|
||||
DATA = SOURCE_XML
|
||||
METADATA = {}
|
||||
|
||||
def test_handle_ajax_dispatch(self):
|
||||
responses = {
|
||||
user.username: self.clients[user.username].post(
|
||||
self.get_url('whatever'),
|
||||
{},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
for user in self.users
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
set([
|
||||
response.status_code
|
||||
for _, response in responses.items()
|
||||
]).pop(),
|
||||
404)
|
||||
|
||||
def tearDown(self):
|
||||
_clear_assets(self.item_module.location)
|
||||
from xmodule.video_module import create_youtube_string
|
||||
|
||||
|
||||
class TestVideoYouTube(TestVideo):
|
||||
@@ -48,7 +15,7 @@ class TestVideoYouTube(TestVideo):
|
||||
|
||||
def test_video_constructor(self):
|
||||
"""Make sure that all parameters extracted correctly from xml"""
|
||||
context = self.item_module.render('student_view').content
|
||||
context = self.item_descriptor.render('student_view').content
|
||||
|
||||
sources = {
|
||||
'main': u'example.mp4',
|
||||
@@ -58,12 +25,12 @@ class TestVideoYouTube(TestVideo):
|
||||
|
||||
expected_context = {
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
'caption_asset_path': '/static/subs/',
|
||||
'show_captions': 'true',
|
||||
'display_name': u'A Name',
|
||||
'end': 3610.0,
|
||||
'id': self.item_module.location.html_id(),
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'show_captions': 'true',
|
||||
'sources': sources,
|
||||
'speed': 'null',
|
||||
'general_speed': 1.0,
|
||||
@@ -71,15 +38,21 @@ class TestVideoYouTube(TestVideo):
|
||||
'saved_video_position': 0.0,
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'track': None,
|
||||
'youtube_streams': _create_youtube_string(self.item_module),
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
|
||||
'youtube_streams': create_youtube_string(self.item_descriptor),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/',
|
||||
'transcript_language': 'en',
|
||||
'transcript_languages': '{"en": "English", "uk": "Ukrainian"}',
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/translation',
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/available_translations',
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
context,
|
||||
self.item_module.xmodule_runtime.render_template('video.html', expected_context),
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context),
|
||||
)
|
||||
|
||||
|
||||
@@ -111,15 +84,14 @@ class TestVideoNonYouTube(TestVideo):
|
||||
u'webm': u'example.webm',
|
||||
}
|
||||
|
||||
context = self.item_module.render('student_view').content
|
||||
context = self.item_descriptor.render('student_view').content
|
||||
expected_context = {
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
'caption_asset_path': '/static/subs/',
|
||||
'show_captions': 'true',
|
||||
'display_name': u'A Name',
|
||||
'end': 3610.0,
|
||||
'id': self.item_module.location.html_id(),
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'sources': sources,
|
||||
'speed': 'null',
|
||||
'general_speed': 1.0,
|
||||
@@ -131,11 +103,19 @@ class TestVideoNonYouTube(TestVideo):
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/',
|
||||
'transcript_language': 'en',
|
||||
'transcript_languages': '{"en": "English"}',
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/translation',
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/available_translations',
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
context,
|
||||
self.item_module.xmodule_runtime.render_template('video.html', expected_context),
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context),
|
||||
)
|
||||
|
||||
|
||||
@@ -192,7 +172,6 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
|
||||
expected_context = {
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
'caption_asset_path': '/static/subs/',
|
||||
'show_captions': 'true',
|
||||
'display_name': u'A Name',
|
||||
'end': 3610.0,
|
||||
@@ -222,20 +201,30 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
)
|
||||
|
||||
self.initialize_module(data=DATA)
|
||||
track_url = self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'download_transcript')
|
||||
track_url = self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/download'
|
||||
|
||||
context = self.item_module.render('student_view').content
|
||||
context = self.item_descriptor.render('student_view').content
|
||||
|
||||
expected_context.update({
|
||||
'transcript_languages': '{"en": "English"}' if self.item_descriptor.sub else '{}',
|
||||
'transcript_language': 'en' if self.item_descriptor.sub else json.dumps(None),
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/translation',
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/available_translations',
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'track': track_url if data['expected_track_url'] == u'a_sub_file.srt.sjson' else data['expected_track_url'],
|
||||
'sub': data['sub'],
|
||||
'id': self.item_module.location.html_id(),
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
context,
|
||||
self.item_module.xmodule_runtime.render_template('video.html', expected_context),
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context),
|
||||
)
|
||||
|
||||
def test_get_html_source(self):
|
||||
@@ -301,7 +290,6 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
|
||||
expected_context = {
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
'caption_asset_path': '/static/subs/',
|
||||
'show_captions': 'true',
|
||||
'display_name': u'A Name',
|
||||
'end': 3610.0,
|
||||
@@ -317,6 +305,8 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/',
|
||||
'transcript_language': 'en',
|
||||
'transcript_languages': '{"en": "English"}',
|
||||
}
|
||||
|
||||
for data in cases:
|
||||
@@ -326,17 +316,23 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
sources=data['sources']
|
||||
)
|
||||
self.initialize_module(data=DATA)
|
||||
context = self.item_module.render('student_view').content
|
||||
context = self.item_descriptor.render('student_view').content
|
||||
|
||||
expected_context.update({
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/translation',
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/available_translations',
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'sources': data['result'],
|
||||
'id': self.item_module.location.html_id(),
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
context,
|
||||
self.item_module.xmodule_runtime.render_template('video.html', expected_context)
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
|
||||
)
|
||||
|
||||
|
||||
@@ -361,9 +357,9 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
|
||||
fields = self.item_descriptor.editable_metadata_fields
|
||||
|
||||
self.assertIn('source', fields)
|
||||
self.assertEqual(self.item_module.source, 'http://example.org/video.mp4')
|
||||
self.assertTrue(self.item_module.download_video)
|
||||
self.assertTrue(self.item_module.source_visible)
|
||||
self.assertEqual(self.item_descriptor.source, 'http://example.org/video.mp4')
|
||||
self.assertTrue(self.item_descriptor.download_video)
|
||||
self.assertTrue(self.item_descriptor.source_visible)
|
||||
|
||||
def test_source_in_html5sources(self):
|
||||
metadata = {
|
||||
@@ -375,10 +371,10 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
|
||||
fields = self.item_descriptor.editable_metadata_fields
|
||||
|
||||
self.assertNotIn('source', fields)
|
||||
self.assertTrue(self.item_module.download_video)
|
||||
self.assertFalse(self.item_module.source_visible)
|
||||
self.assertTrue(self.item_descriptor.download_video)
|
||||
self.assertFalse(self.item_descriptor.source_visible)
|
||||
|
||||
@patch('xmodule.x_module.XModuleDescriptor.editable_metadata_fields', new_callable=PropertyMock)
|
||||
@patch('xmodule.video_module.VideoDescriptor.editable_metadata_fields', new_callable=PropertyMock)
|
||||
def test_download_video_is_explicitly_set(self, mock_editable_fields):
|
||||
mock_editable_fields.return_value = {
|
||||
'download_video': {
|
||||
@@ -445,9 +441,9 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
|
||||
fields = self.item_descriptor.editable_metadata_fields
|
||||
|
||||
self.assertIn('source', fields)
|
||||
self.assertFalse(self.item_module.download_video)
|
||||
self.assertTrue(self.item_module.source_visible)
|
||||
self.assertTrue(self.item_module.download_track)
|
||||
self.assertFalse(self.item_descriptor.download_video)
|
||||
self.assertTrue(self.item_descriptor.source_visible)
|
||||
self.assertTrue(self.item_descriptor.download_track)
|
||||
|
||||
def test_source_is_empty(self):
|
||||
metadata = {
|
||||
@@ -459,152 +455,4 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
|
||||
fields = self.item_descriptor.editable_metadata_fields
|
||||
|
||||
self.assertNotIn('source', fields)
|
||||
self.assertFalse(self.item_module.download_video)
|
||||
|
||||
|
||||
class TestVideoGetTranscriptsMethod(TestVideo):
|
||||
"""
|
||||
Make sure that `get_transcript` method works correctly
|
||||
"""
|
||||
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
</video>
|
||||
"""
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
}
|
||||
METADATA = {}
|
||||
|
||||
def test_good_transcript(self):
|
||||
self.item_module.render('student_view')
|
||||
item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
good_sjson = _create_file(content=textwrap.dedent("""\
|
||||
{
|
||||
"start": [
|
||||
270,
|
||||
2720
|
||||
],
|
||||
"end": [
|
||||
2720,
|
||||
5430
|
||||
],
|
||||
"text": [
|
||||
"Hi, welcome to Edx.",
|
||||
"Let's start with what is on your screen right now."
|
||||
]
|
||||
}
|
||||
"""))
|
||||
|
||||
_upload_file(good_sjson, self.item_module.location)
|
||||
subs_id = _get_subs_id(good_sjson.name)
|
||||
|
||||
text = item.get_transcript(subs_id)
|
||||
expected_text = textwrap.dedent("""\
|
||||
0
|
||||
00:00:00,270 --> 00:00:02,720
|
||||
Hi, welcome to Edx.
|
||||
|
||||
1
|
||||
00:00:02,720 --> 00:00:05,430
|
||||
Let's start with what is on your screen right now.
|
||||
|
||||
""")
|
||||
|
||||
self.assertEqual(text, expected_text)
|
||||
|
||||
def test_not_found_error(self):
|
||||
self.item_module.render('student_view')
|
||||
item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
with self.assertRaises(NotFoundError):
|
||||
item.get_transcript('wrong')
|
||||
|
||||
def test_value_error(self):
|
||||
self.item_module.render('student_view')
|
||||
item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
good_sjson = _create_file(content='bad content')
|
||||
|
||||
_upload_file(good_sjson, self.item_module.location)
|
||||
subs_id = _get_subs_id(good_sjson.name)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
item.get_transcript(subs_id)
|
||||
|
||||
def test_key_error(self):
|
||||
self.item_module.render('student_view')
|
||||
item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
good_sjson = _create_file(content="""
|
||||
{
|
||||
"start": [
|
||||
270,
|
||||
2720
|
||||
],
|
||||
"end": [
|
||||
2720,
|
||||
5430
|
||||
]
|
||||
}
|
||||
""")
|
||||
|
||||
_upload_file(good_sjson, self.item_module.location)
|
||||
subs_id = _get_subs_id(good_sjson.name)
|
||||
|
||||
with self.assertRaises(KeyError):
|
||||
item.get_transcript(subs_id)
|
||||
|
||||
|
||||
def _clear_assets(location):
|
||||
store = contentstore()
|
||||
|
||||
content_location = StaticContent.compute_location(
|
||||
location.org, location.course, location.name
|
||||
)
|
||||
|
||||
assets, __ = store.get_all_content_for_course(content_location)
|
||||
for asset in assets:
|
||||
asset_location = Location(asset["_id"])
|
||||
id = StaticContent.get_id_from_location(asset_location)
|
||||
store.delete(id)
|
||||
|
||||
def _get_subs_id(filename):
|
||||
basename = os.path.splitext(os.path.basename(filename))[0]
|
||||
return basename.replace('subs_', '').replace('.srt', '')
|
||||
|
||||
def _create_file(content=''):
|
||||
sjson_file = tempfile.NamedTemporaryFile(prefix="subs_", suffix=".srt.sjson")
|
||||
sjson_file.content_type = 'application/json'
|
||||
sjson_file.write(textwrap.dedent(content))
|
||||
sjson_file.seek(0)
|
||||
|
||||
return sjson_file
|
||||
|
||||
def _upload_file(file, location):
|
||||
filename = 'subs_{}.srt.sjson'.format(_get_subs_id(file.name))
|
||||
mime_type = file.content_type
|
||||
|
||||
content_location = StaticContent.compute_location(
|
||||
location.org, location.course, filename
|
||||
)
|
||||
|
||||
sc_partial = partial(StaticContent, content_location, filename, mime_type)
|
||||
content = sc_partial(file.read())
|
||||
|
||||
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(
|
||||
content,
|
||||
tempfile_path=None
|
||||
)
|
||||
del_cached_content(thumbnail_location)
|
||||
|
||||
if thumbnail_content is not None:
|
||||
content.thumbnail_location = thumbnail_location
|
||||
|
||||
contentstore().save(content)
|
||||
del_cached_content(content.location)
|
||||
self.assertFalse(self.item_descriptor.download_video)
|
||||
|
||||
@@ -32,6 +32,7 @@ SOURCE_XML = """
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
<transcript language="uk" src="ukrainian_translation.srt" />
|
||||
</video>
|
||||
"""
|
||||
|
||||
|
||||
@@ -242,12 +242,12 @@ class TestWordCloud(BaseTestXmodule):
|
||||
|
||||
def test_word_cloud_constructor(self):
|
||||
"""Make sure that all parameters extracted correclty from xml"""
|
||||
fragment = self.runtime.render(self.item_module, 'student_view')
|
||||
fragment = self.runtime.render(self.item_descriptor, 'student_view')
|
||||
|
||||
expected_context = {
|
||||
'ajax_url': self.item_module.xmodule_runtime.ajax_url,
|
||||
'element_class': self.item_module.location.category,
|
||||
'element_id': self.item_module.location.html_id(),
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url,
|
||||
'element_class': self.item_descriptor.location.category,
|
||||
'element_id': self.item_descriptor.location.html_id(),
|
||||
'num_inputs': 5, # default value
|
||||
'submitted': False # default value
|
||||
}
|
||||
|
||||
@@ -1287,3 +1287,193 @@ MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60
|
||||
|
||||
##### LMS DEADLINE DISPLAY TIME_ZONE #######
|
||||
TIME_ZONE_DISPLAYED_FOR_DEADLINES = 'UTC'
|
||||
|
||||
|
||||
# Source:
|
||||
# http://loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt according to http://en.wikipedia.org/wiki/ISO_639-1
|
||||
ALL_LANGUAGES = (
|
||||
[u"aa", u"Afar"],
|
||||
[u"ab", u"Abkhazian"],
|
||||
[u"af", u"Afrikaans"],
|
||||
[u"ak", u"Akan"],
|
||||
[u"sq", u"Albanian"],
|
||||
[u"am", u"Amharic"],
|
||||
[u"ar", u"Arabic"],
|
||||
[u"an", u"Aragonese"],
|
||||
[u"hy", u"Armenian"],
|
||||
[u"as", u"Assamese"],
|
||||
[u"av", u"Avaric"],
|
||||
[u"ae", u"Avestan"],
|
||||
[u"ay", u"Aymara"],
|
||||
[u"az", u"Azerbaijani"],
|
||||
[u"ba", u"Bashkir"],
|
||||
[u"bm", u"Bambara"],
|
||||
[u"eu", u"Basque"],
|
||||
[u"be", u"Belarusian"],
|
||||
[u"bn", u"Bengali"],
|
||||
[u"bh", u"Bihari languages"],
|
||||
[u"bi", u"Bislama"],
|
||||
[u"bs", u"Bosnian"],
|
||||
[u"br", u"Breton"],
|
||||
[u"bg", u"Bulgarian"],
|
||||
[u"my", u"Burmese"],
|
||||
[u"ca", u"Catalan; Valencian"],
|
||||
[u"ch", u"Chamorro"],
|
||||
[u"ce", u"Chechen"],
|
||||
[u"zh", u"Chinese"],
|
||||
[u"cu", u"Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic"],
|
||||
[u"cv", u"Chuvash"],
|
||||
[u"kw", u"Cornish"],
|
||||
[u"co", u"Corsican"],
|
||||
[u"cr", u"Cree"],
|
||||
[u"cs", u"Czech"],
|
||||
[u"da", u"Danish"],
|
||||
[u"dv", u"Divehi; Dhivehi; Maldivian"],
|
||||
[u"nl", u"Dutch; Flemish"],
|
||||
[u"dz", u"Dzongkha"],
|
||||
[u"en", u"English"],
|
||||
[u"eo", u"Esperanto"],
|
||||
[u"et", u"Estonian"],
|
||||
[u"ee", u"Ewe"],
|
||||
[u"fo", u"Faroese"],
|
||||
[u"fj", u"Fijian"],
|
||||
[u"fi", u"Finnish"],
|
||||
[u"fr", u"French"],
|
||||
[u"fy", u"Western Frisian"],
|
||||
[u"ff", u"Fulah"],
|
||||
[u"ka", u"Georgian"],
|
||||
[u"de", u"German"],
|
||||
[u"gd", u"Gaelic; Scottish Gaelic"],
|
||||
[u"ga", u"Irish"],
|
||||
[u"gl", u"Galician"],
|
||||
[u"gv", u"Manx"],
|
||||
[u"el", u"Greek, Modern (1453-)"],
|
||||
[u"gn", u"Guarani"],
|
||||
[u"gu", u"Gujarati"],
|
||||
[u"ht", u"Haitian; Haitian Creole"],
|
||||
[u"ha", u"Hausa"],
|
||||
[u"he", u"Hebrew"],
|
||||
[u"hz", u"Herero"],
|
||||
[u"hi", u"Hindi"],
|
||||
[u"ho", u"Hiri Motu"],
|
||||
[u"hr", u"Croatian"],
|
||||
[u"hu", u"Hungarian"],
|
||||
[u"ig", u"Igbo"],
|
||||
[u"is", u"Icelandic"],
|
||||
[u"io", u"Ido"],
|
||||
[u"ii", u"Sichuan Yi; Nuosu"],
|
||||
[u"iu", u"Inuktitut"],
|
||||
[u"ie", u"Interlingue; Occidental"],
|
||||
[u"ia", u"Interlingua (International Auxiliary Language Association)"],
|
||||
[u"id", u"Indonesian"],
|
||||
[u"ik", u"Inupiaq"],
|
||||
[u"it", u"Italian"],
|
||||
[u"jv", u"Javanese"],
|
||||
[u"ja", u"Japanese"],
|
||||
[u"kl", u"Kalaallisut; Greenlandic"],
|
||||
[u"kn", u"Kannada"],
|
||||
[u"ks", u"Kashmiri"],
|
||||
[u"kr", u"Kanuri"],
|
||||
[u"kk", u"Kazakh"],
|
||||
[u"km", u"Central Khmer"],
|
||||
[u"ki", u"Kikuyu; Gikuyu"],
|
||||
[u"rw", u"Kinyarwanda"],
|
||||
[u"ky", u"Kirghiz; Kyrgyz"],
|
||||
[u"kv", u"Komi"],
|
||||
[u"kg", u"Kongo"],
|
||||
[u"ko", u"Korean"],
|
||||
[u"kj", u"Kuanyama; Kwanyama"],
|
||||
[u"ku", u"Kurdish"],
|
||||
[u"lo", u"Lao"],
|
||||
[u"la", u"Latin"],
|
||||
[u"lv", u"Latvian"],
|
||||
[u"li", u"Limburgan; Limburger; Limburgish"],
|
||||
[u"ln", u"Lingala"],
|
||||
[u"lt", u"Lithuanian"],
|
||||
[u"lb", u"Luxembourgish; Letzeburgesch"],
|
||||
[u"lu", u"Luba-Katanga"],
|
||||
[u"lg", u"Ganda"],
|
||||
[u"mk", u"Macedonian"],
|
||||
[u"mh", u"Marshallese"],
|
||||
[u"ml", u"Malayalam"],
|
||||
[u"mi", u"Maori"],
|
||||
[u"mr", u"Marathi"],
|
||||
[u"ms", u"Malay"],
|
||||
[u"mg", u"Malagasy"],
|
||||
[u"mt", u"Maltese"],
|
||||
[u"mn", u"Mongolian"],
|
||||
[u"na", u"Nauru"],
|
||||
[u"nv", u"Navajo; Navaho"],
|
||||
[u"nr", u"Ndebele, South; South Ndebele"],
|
||||
[u"nd", u"Ndebele, North; North Ndebele"],
|
||||
[u"ng", u"Ndonga"],
|
||||
[u"ne", u"Nepali"],
|
||||
[u"nn", u"Norwegian Nynorsk; Nynorsk, Norwegian"],
|
||||
[u"nb", u"Bokmål, Norwegian; Norwegian Bokmål"],
|
||||
[u"no", u"Norwegian"],
|
||||
[u"ny", u"Chichewa; Chewa; Nyanja"],
|
||||
[u"oc", u"Occitan (post 1500); Provençal"],
|
||||
[u"oj", u"Ojibwa"],
|
||||
[u"or", u"Oriya"],
|
||||
[u"om", u"Oromo"],
|
||||
[u"os", u"Ossetian; Ossetic"],
|
||||
[u"pa", u"Panjabi; Punjabi"],
|
||||
[u"fa", u"Persian"],
|
||||
[u"pi", u"Pali"],
|
||||
[u"pl", u"Polish"],
|
||||
[u"pt", u"Portuguese"],
|
||||
[u"ps", u"Pushto; Pashto"],
|
||||
[u"qu", u"Quechua"],
|
||||
[u"rm", u"Romansh"],
|
||||
[u"ro", u"Romanian; Moldavian; Moldovan"],
|
||||
[u"rn", u"Rundi"],
|
||||
[u"ru", u"Russian"],
|
||||
[u"sg", u"Sango"],
|
||||
[u"sa", u"Sanskrit"],
|
||||
[u"si", u"Sinhala; Sinhalese"],
|
||||
[u"sk", u"Slovak"],
|
||||
[u"sl", u"Slovenian"],
|
||||
[u"se", u"Northern Sami"],
|
||||
[u"sm", u"Samoan"],
|
||||
[u"sn", u"Shona"],
|
||||
[u"sd", u"Sindhi"],
|
||||
[u"so", u"Somali"],
|
||||
[u"st", u"Sotho, Southern"],
|
||||
[u"es", u"Spanish; Castilian"],
|
||||
[u"sc", u"Sardinian"],
|
||||
[u"sr", u"Serbian"],
|
||||
[u"ss", u"Swati"],
|
||||
[u"su", u"Sundanese"],
|
||||
[u"sw", u"Swahili"],
|
||||
[u"sv", u"Swedish"],
|
||||
[u"ty", u"Tahitian"],
|
||||
[u"ta", u"Tamil"],
|
||||
[u"tt", u"Tatar"],
|
||||
[u"te", u"Telugu"],
|
||||
[u"tg", u"Tajik"],
|
||||
[u"tl", u"Tagalog"],
|
||||
[u"th", u"Thai"],
|
||||
[u"bo", u"Tibetan"],
|
||||
[u"ti", u"Tigrinya"],
|
||||
[u"to", u"Tonga (Tonga Islands)"],
|
||||
[u"tn", u"Tswana"],
|
||||
[u"ts", u"Tsonga"],
|
||||
[u"tk", u"Turkmen"],
|
||||
[u"tr", u"Turkish"],
|
||||
[u"tw", u"Twi"],
|
||||
[u"ug", u"Uighur; Uyghur"],
|
||||
[u"uk", u"Ukrainian"],
|
||||
[u"ur", u"Urdu"],
|
||||
[u"uz", u"Uzbek"],
|
||||
[u"ve", u"Venda"],
|
||||
[u"vi", u"Vietnamese"],
|
||||
[u"vo", u"Volapük"],
|
||||
[u"cy", u"Welsh"],
|
||||
[u"wa", u"Walloon"],
|
||||
[u"wo", u"Wolof"],
|
||||
[u"xh", u"Xhosa"],
|
||||
[u"yi", u"Yiddish"],
|
||||
[u"yo", u"Yoruba"],
|
||||
[u"za", u"Zhuang; Chuang"],
|
||||
[u"zu", u"Zulu"]
|
||||
)
|
||||
|
||||
@@ -25,10 +25,13 @@
|
||||
data-saved-video-position="${saved_video_position}"
|
||||
data-start="${start}"
|
||||
data-end="${end}"
|
||||
data-caption-asset-path="${caption_asset_path}"
|
||||
data-transcript-language="${transcript_language}"
|
||||
data-transcript-languages='${transcript_languages}'
|
||||
data-autoplay="${autoplay}"
|
||||
data-yt-test-timeout="${yt_test_timeout}"
|
||||
data-yt-test-url="${yt_test_url}"
|
||||
data-transcript-translation-url="${transcript_translation_url}"
|
||||
data-transcript-available-translations-url="${transcript_available_translations_url}"
|
||||
|
||||
## For now, the option "data-autohide-html5" is hard coded. This option
|
||||
## either enables or disables autohiding of controls and captions on mouse
|
||||
@@ -67,12 +70,12 @@
|
||||
<li><div class="vidtime">0:00 / 0:00</div></li>
|
||||
</ul>
|
||||
<div class="secondary-controls">
|
||||
<div class="speeds">
|
||||
<div class="speeds menu-container">
|
||||
<a href="#" title="${_('Speeds')}" role="button" aria-disabled="false">
|
||||
<h3>${_('Speed')}</h3>
|
||||
<p class="active"></p>
|
||||
</a>
|
||||
<ol class="video_speeds" role="menu"></ol>
|
||||
<ol class="video_speeds menu" role="menu"></ol>
|
||||
</div>
|
||||
<div class="volume">
|
||||
<a href="#" title="${_('Volume')}" role="button" aria-disabled="false"></a>
|
||||
@@ -83,7 +86,10 @@
|
||||
<a href="#" class="add-fullscreen" title="${_('Fill browser')}" role="button" aria-disabled="false">${_('Fill browser')}</a>
|
||||
<a href="#" class="quality_control" title="${_('HD off')}" role="button" aria-disabled="false">${_('HD off')}</a>
|
||||
|
||||
<a href="#" class="hide-subtitles" title="${_('Turn off captions')}" role="button" aria-disabled="false">${_('Turn off captions')}</a>
|
||||
<div class="lang menu-container">
|
||||
<a href="#" class="hide-subtitles" title="${_('Turn off captions')}" role="button" aria-
|
||||
disabled="false">${_('Turn off captions')}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user