Commit e817fb4c by Lyla Fischer

Merge pull request #637 from edx/vaxxxa/videoalpha_to_video

Migration videoalpha module to one main video module
parents c727d43c 74f3595d
...@@ -210,27 +210,6 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time): ...@@ -210,27 +210,6 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time):
time.sleep(float(1)) time.sleep(float(1))
@step('I have created a Video component$')
def i_created_a_video_component(step):
world.create_component_instance(
step, '.large-video-icon',
'video',
'.xmodule_VideoModule',
has_multiple_templates=False
)
@step('I have created a Video Alpha component$')
def i_created_video_alpha(step):
step.given('I have enabled the videoalpha advanced module')
world.css_click('a.course-link')
step.given('I have added a new subsection')
step.given('I expand the first section')
world.css_click('a.new-unit-item')
world.css_click('.large-advanced-icon')
world.click_component_from_menu('videoalpha', None, '.xmodule_VideoAlphaModule')
@step('I have enabled the (.*) advanced module$') @step('I have enabled the (.*) advanced module$')
def i_enabled_the_advanced_module(step, module): def i_enabled_the_advanced_module(step, module):
step.given('I have opened a new course section in Studio') step.given('I have opened a new course section in Studio')
...@@ -248,16 +227,6 @@ def open_new_unit(step): ...@@ -248,16 +227,6 @@ def open_new_unit(step):
world.css_click('a.new-unit-item') world.css_click('a.new-unit-item')
@step('when I view the (video.*) it (.*) show the captions')
def shows_captions(_step, video_type, show_captions):
# Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions')
if show_captions == 'does not':
assert world.css_has_class('.%s' % video_type, 'closed')
else:
assert world.is_css_not_present('.%s.closed' % video_type)
@step('the save button is disabled$') @step('the save button is disabled$')
def save_button_disabled(step): def save_button_disabled(step):
button_css = '.action-save' button_css = '.action-save'
......
Feature: Video Component Editor Feature: Video Component Editor
As a course author, I want to be able to create video components. As a course author, I want to be able to create video components.
Scenario: User can view metadata Scenario: User can view Video metadata
Given I have created a Video component Given I have created a Video component
And I edit and select Settings And I edit the component
Then I see the correct settings and default values Then I see the correct video settings and default values
Scenario: User can modify display name Scenario: User can modify Video display name
Given I have created a Video component Given I have created a Video component
And I edit and select Settings And I edit the component
Then I can modify the display name Then I can modify the display name
And my display name change is persisted on save And my video display name change is persisted on save
Scenario: Captions are hidden when "show captions" is false Scenario: Captions are hidden when "show captions" is false
Given I have created a Video component Given I have created a Video component
......
...@@ -2,18 +2,7 @@ ...@@ -2,18 +2,7 @@
# pylint: disable=C0111 # pylint: disable=C0111
from lettuce import world, step from lettuce import world, step
from terrain.steps import reload_the_page
@step('I see the correct settings and default values$')
def i_see_the_correct_settings_and_values(step):
world.verify_all_setting_entries([['Default Speed', 'OEoXaMPEzfM', False],
['Display Name', 'Video', False],
['Download Track', '', False],
['Download Video', '', False],
['Show Captions', 'True', False],
['Speed: .75x', '', False],
['Speed: 1.25x', '', False],
['Speed: 1.5x', '', False]])
@step('I have set "show captions" to (.*)') @step('I have set "show captions" to (.*)')
...@@ -24,9 +13,19 @@ def set_show_captions(step, setting): ...@@ -24,9 +13,19 @@ def set_show_captions(step, setting):
world.css_click('a.save-button') world.css_click('a.save-button')
@step('I see the correct videoalpha settings and default values$') @step('when I view the (video.*) it (.*) show the captions')
def correct_videoalpha_settings(_step): def shows_captions(_step, video_type, show_captions):
world.verify_all_setting_entries([['Display Name', 'Video Alpha', False], # Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions')
if show_captions == 'does not':
assert world.css_has_class('.%s' % video_type, 'closed')
else:
assert world.is_css_not_present('.%s.closed' % video_type)
@step('I see the correct video settings and default values$')
def correct_video_settings(_step):
world.verify_all_setting_entries([['Display Name', 'Video', False],
['Download Track', '', False], ['Download Track', '', False],
['Download Video', '', False], ['Download Video', '', False],
['End Time', '0', False], ['End Time', '0', False],
...@@ -38,3 +37,12 @@ def correct_videoalpha_settings(_step): ...@@ -38,3 +37,12 @@ def correct_videoalpha_settings(_step):
['Youtube ID for .75x speed', '', False], ['Youtube ID for .75x speed', '', False],
['Youtube ID for 1.25x speed', '', False], ['Youtube ID for 1.25x speed', '', False],
['Youtube ID for 1.5x speed', '', False]]) ['Youtube ID for 1.5x speed', '', False]])
@step('my video display name change is persisted on save')
def video_name_persisted(step):
world.css_click('a.save-button')
reload_the_page(step)
world.edit_component()
world.verify_setting_entry(world.get_setting_entry('Display Name'), 'Display Name', '3.4', True)
Feature: Video Component Feature: Video Component
As a course author, I want to be able to view my created videos in Studio. As a course author, I want to be able to view my created videos in Studio.
# Video Alpha Features will work in Firefox only when Firefox is the active window
Scenario: Autoplay is disabled in Studio Scenario: Autoplay is disabled in Studio
Given I have created a Video component Given I have created a Video component
Then when I view the video it does not have autoplay enabled Then when I view the video it does not have autoplay enabled
...@@ -23,32 +24,6 @@ Feature: Video Component ...@@ -23,32 +24,6 @@ Feature: Video Component
And I have toggled captions And I have toggled captions
Then when I view the video it does show the captions Then when I view the video it does show the captions
# Video Alpha Features will work in Firefox only when Firefox is the active window
Scenario: Autoplay is disabled in Studio for Video Alpha
Given I have created a Video Alpha component
Then when I view the videoalpha it does not have autoplay enabled
Scenario: User can view Video Alpha metadata
Given I have created a Video Alpha component
And I edit the component
Then I see the correct videoalpha settings and default values
Scenario: User can modify Video Alpha display name
Given I have created a Video Alpha component
And I edit the component
Then I can modify the display name
And my videoalpha display name change is persisted on save
Scenario: Video Alpha captions are hidden when "show captions" is false
Given I have created a Video Alpha component
And I have set "show captions" to False
Then when I view the videoalpha it does not show the captions
Scenario: Video Alpha captions are shown when "show captions" is true
Given I have created a Video Alpha component
And I have set "show captions" to True
Then when I view the videoalpha it does show the captions
Scenario: Video data is shown correctly Scenario: Video data is shown correctly
Given I have created a video with only XML data Given I have created a video with only XML data
Then the correct Youtube video is shown Then the correct Youtube video is shown
...@@ -9,6 +9,16 @@ from contentstore.utils import get_modulestore ...@@ -9,6 +9,16 @@ from contentstore.utils import get_modulestore
############### ACTIONS #################### ############### ACTIONS ####################
@step('I have created a Video component$')
def i_created_a_video_component(step):
world.create_component_instance(
step, '.large-video-icon',
'video',
'.xmodule_VideoModule',
has_multiple_templates=False
)
@step('when I view the (.*) it does not have autoplay enabled') @step('when I view the (.*) it does not have autoplay enabled')
def does_not_autoplay(_step, video_type): def does_not_autoplay(_step, video_type):
assert world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False' assert world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False'
...@@ -22,6 +32,11 @@ def video_takes_a_single_click(_step): ...@@ -22,6 +32,11 @@ def video_takes_a_single_click(_step):
assert(world.is_css_present('.xmodule_VideoModule')) assert(world.is_css_present('.xmodule_VideoModule'))
@step('I edit the component')
def i_edit_the_component(_step):
world.edit_component()
@step('I have (hidden|toggled) captions') @step('I have (hidden|toggled) captions')
def hide_or_show_captions(step, shown): def hide_or_show_captions(step, shown):
button_css = 'a.hide-subtitles' button_css = 'a.hide-subtitles'
...@@ -38,18 +53,6 @@ def hide_or_show_captions(step, shown): ...@@ -38,18 +53,6 @@ def hide_or_show_captions(step, shown):
button.mouse_out() button.mouse_out()
world.css_click(button_css) world.css_click(button_css)
@step('I edit the component')
def i_edit_the_component(_step):
world.edit_component()
@step('my videoalpha display name change is persisted on save')
def videoalpha_name_persisted(step):
world.css_click('a.save-button')
reload_the_page(step)
world.edit_component()
world.verify_setting_entry(world.get_setting_entry('Display Name'), 'Display Name', '3.4', True)
@step('I have created a video with only XML data') @step('I have created a video with only XML data')
def xml_only_video(step): def xml_only_video(step):
...@@ -84,4 +87,5 @@ def xml_only_video(step): ...@@ -84,4 +87,5 @@ def xml_only_video(step):
@step('The correct Youtube video is shown') @step('The correct Youtube video is shown')
def the_youtube_video_is_shown(_step): def the_youtube_video_is_shown(_step):
ele = world.css_find('.video').first ele = world.css_find('.video').first
assert ele['data-youtube-id-1-0'] == world.scenario_dict['YOUTUBE_ID'] assert ele['data-streams'].split(':')[1] == world.scenario_dict['YOUTUBE_ID']
...@@ -107,8 +107,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -107,8 +107,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
expected_types is the list of elements that should appear on the page. expected_types is the list of elements that should appear on the page.
expected_types and component_types should be similar, but not expected_types and component_types should be similar, but not
exactly the same -- for example, 'videoalpha' in exactly the same -- for example, 'video' in
component_types should cause 'Video Alpha' to be present. component_types should cause 'Video' to be present.
""" """
store = modulestore('direct') store = modulestore('direct')
import_from_xml(store, 'common/test/data/', ['simple']) import_from_xml(store, 'common/test/data/', ['simple'])
...@@ -136,14 +136,13 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -136,14 +136,13 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_advanced_components_in_edit_unit(self): def test_advanced_components_in_edit_unit(self):
# This could be made better, but for now let's just assert that we see the advanced modules mentioned in the page # This could be made better, but for now let's just assert that we see the advanced modules mentioned in the page
# response HTML # response HTML
self.check_components_on_page(ADVANCED_COMPONENT_TYPES, ['Video Alpha', self.check_components_on_page(ADVANCED_COMPONENT_TYPES, ['Word cloud',
'Word cloud',
'Annotation', 'Annotation',
'Open Response Assessment', 'Open Response Assessment',
'Peer Grading Interface']) 'Peer Grading Interface'])
def test_advanced_components_require_two_clicks(self): def test_advanced_components_require_two_clicks(self):
self.check_components_on_page(['videoalpha'], ['Video Alpha']) self.check_components_on_page(['word_cloud'], ['Word cloud'])
def test_malformed_edit_unit_request(self): def test_malformed_edit_unit_request(self):
store = modulestore('direct') store = modulestore('direct')
...@@ -1597,12 +1596,15 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1597,12 +1596,15 @@ class ContentStoreTest(ModuleStoreTestCase):
class MetadataSaveTestCase(ModuleStoreTestCase): class MetadataSaveTestCase(ModuleStoreTestCase):
""" """Test that metadata is correctly cached and decached."""
Test that metadata is correctly decached.
"""
def setUp(self): def setUp(self):
sample_xml = ''' CourseFactory.create(
org='edX', course='999', display_name='Robot Super Course')
course_location = Location(
['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
video_sample_xml = '''
<video display_name="Test Video" <video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8" youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
show_captions="false" show_captions="false"
...@@ -1612,19 +1614,17 @@ class MetadataSaveTestCase(ModuleStoreTestCase): ...@@ -1612,19 +1614,17 @@ class MetadataSaveTestCase(ModuleStoreTestCase):
<track src="http://www.example.com/track"/> <track src="http://www.example.com/track"/>
</video> </video>
''' '''
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course') self.video_descriptor = ItemFactory.create(
course_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None]) parent_location=course_location, category='video',
data={'data': video_sample_xml}
model_data = {'data': sample_xml} )
self.descriptor = ItemFactory.create(parent_location=course_location, category='video', data=model_data)
def test_metadata_persistence(self): def test_metadata_not_persistence(self):
""" """
Test that descriptors which set metadata fields in their Test that descriptors which set metadata fields in their
constructor are correctly persisted. constructor are correctly deleted.
""" """
# We should start with a source field, from the XML's <source/> tag self.assertIn('html5_sources', own_metadata(self.video_descriptor))
self.assertIn('source', own_metadata(self.descriptor))
attrs_to_strip = { attrs_to_strip = {
'show_captions', 'show_captions',
'youtube_id_1_0', 'youtube_id_1_0',
...@@ -1634,23 +1634,27 @@ class MetadataSaveTestCase(ModuleStoreTestCase): ...@@ -1634,23 +1634,27 @@ class MetadataSaveTestCase(ModuleStoreTestCase):
'start_time', 'start_time',
'end_time', 'end_time',
'source', 'source',
'html5_sources',
'track' 'track'
} }
# We strip out all metadata fields to reproduce a bug where
# constructors which set their fields (e.g. Video) didn't have fields = self.video_descriptor.fields
# those changes persisted. So in the end we have the XML data location = self.video_descriptor.location
# in `descriptor.data`, but not in the individual fields
fields = self.descriptor.fields
for field in fields: for field in fields:
if field.name in attrs_to_strip: if field.name in attrs_to_strip:
field.delete_from(self.descriptor) field.delete_from(self.video_descriptor)
# Assert that we correctly stripped the field self.assertNotIn('html5_sources', own_metadata(self.video_descriptor))
self.assertNotIn('source', own_metadata(self.descriptor)) get_modulestore(location).update_metadata(
get_modulestore(self.descriptor.location).update_metadata( location,
self.descriptor.location, own_metadata(self.video_descriptor)
own_metadata(self.descriptor)
) )
module = get_modulestore(self.descriptor.location).get_item(self.descriptor.location) module = get_modulestore(location).get_item(location)
# Assert that get_item correctly sets the metadata
self.assertIn('source', own_metadata(module)) self.assertNotIn('html5_sources', own_metadata(module))
def test_metadata_persistence(self):
# TODO: create the same test as `test_metadata_not_persistence`,
# but check persistence for some other module.
pass
...@@ -49,7 +49,6 @@ NOTE_COMPONENT_TYPES = ['notes'] ...@@ -49,7 +49,6 @@ NOTE_COMPONENT_TYPES = ['notes']
ADVANCED_COMPONENT_TYPES = [ ADVANCED_COMPONENT_TYPES = [
'annotatable', 'annotatable',
'word_cloud', 'word_cloud',
'videoalpha',
'graphical_slider_tool' 'graphical_slider_tool'
] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES ] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_CATEGORY = 'advanced'
......
...@@ -39,9 +39,6 @@ MITX_FEATURES = { ...@@ -39,9 +39,6 @@ MITX_FEATURES = {
'AUTH_USE_MIT_CERTIFICATES': False, 'AUTH_USE_MIT_CERTIFICATES': False,
# do not display video when running automated acceptance tests
'STUB_VIDEO_FOR_TESTING': False,
# email address for studio staff (eg to request course creation) # email address for studio staff (eg to request course creation)
'STUDIO_REQUEST_EMAIL': '', 'STUDIO_REQUEST_EMAIL': '',
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// ==================== // ====================
// Video Alpha // Video Alpha
.xmodule_VideoAlphaModule { .xmodule_VideoModule {
// display mode // display mode
&.xmodule_display { &.xmodule_display {
......
...@@ -40,7 +40,7 @@ setup( ...@@ -40,7 +40,7 @@ setup(
"timelimit = xmodule.timelimit_module:TimeLimitDescriptor", "timelimit = xmodule.timelimit_module:TimeLimitDescriptor",
"vertical = xmodule.vertical_module:VerticalDescriptor", "vertical = xmodule.vertical_module:VerticalDescriptor",
"video = xmodule.video_module:VideoDescriptor", "video = xmodule.video_module:VideoDescriptor",
"videoalpha = xmodule.videoalpha_module:VideoAlphaDescriptor", "videoalpha = xmodule.video_module:VideoDescriptor",
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"videosequence = xmodule.seq_module:SequenceDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor",
"discussion = xmodule.discussion_module:DiscussionDescriptor", "discussion = xmodule.discussion_module:DiscussionDescriptor",
......
...@@ -10,11 +10,30 @@ div.video { ...@@ -10,11 +10,30 @@ div.video {
padding: 12px; padding: 12px;
border-radius: 5px; border-radius: 5px;
div.tc-wrapper {
position: relative;
@include clearfix;
}
article.video-wrapper { article.video-wrapper {
float: left; float: left;
margin-right: flex-gutter(9); margin-right: flex-gutter(9);
width: flex-grid(6, 9); width: flex-grid(6, 9);
background-color: black;
position: relative;
div.video-player-pre {
height: 50px;
background-color: black;
}
div.video-player-post {
height: 50px;
background-color: black;
}
section.video-player { section.video-player {
height: 0; height: 0;
overflow: hidden; overflow: hidden;
...@@ -52,10 +71,19 @@ div.video { ...@@ -52,10 +71,19 @@ div.video {
border-radius: 0; border-radius: 0;
border-top: 1px solid #000; border-top: 1px solid #000;
box-shadow: inset 0 1px 0 #eee, 0 1px 0 #555; box-shadow: inset 0 1px 0 #eee, 0 1px 0 #555;
height: 7px; position: absolute;
z-index: 1;
bottom: 100%;
left: 0;
right: 0;
height: 14px;
margin-left: -1px; margin-left: -1px;
margin-right: -1px; margin-right: -1px;
@include transition(height 2.0s ease-in-out 0s); -webkit-transition: -webkit-transform 0.7s ease-in-out;
-moz-transition: -moz-transform 0.7s ease-in-out;
-ms-transition: -ms-transform 0.7s ease-in-out;
transition: transform 0.7s ease-in-out;
@include transform(scaleY(0.5) translate3d(0, 50%, 0));
div.ui-widget-header { div.ui-widget-header {
background: #777; background: #777;
...@@ -66,14 +94,18 @@ div.video { ...@@ -66,14 +94,18 @@ div.video {
background: $pink url(../images/slider-handle.png) center center no-repeat; background: $pink url(../images/slider-handle.png) center center no-repeat;
background-size: 50%; background-size: 50%;
border: 1px solid darken($pink, 20%); border: 1px solid darken($pink, 20%);
border-radius: 15px; border-radius: 50%;
box-shadow: inset 0 1px 0 lighten($pink, 10%); box-shadow: inset 0 1px 0 lighten($pink, 10%);
cursor: pointer; cursor: pointer;
height: 15px; height: 20px;
margin-left: -7px; margin-left: 0;
top: -4px; top: 0;
@include transition(height 2.0s ease-in-out 0s, width 2.0s ease-in-out 0s); -webkit-transition: -webkit-transform 0.7s ease-in-out;
width: 15px; -moz-transition: -moz-transform 0.7s ease-in-out;
-ms-transition: -ms-transform 0.7s ease-in-out;
transition: transform 0.7s ease-in-out;
@include transform(scale(.7, 1.3) translate3d(-80%, -15%, 0));
width: 20px;
&:focus, &:hover { &:focus, &:hover {
background-color: lighten($pink, 10%); background-color: lighten($pink, 10%);
...@@ -101,7 +133,6 @@ div.video { ...@@ -101,7 +133,6 @@ div.video {
line-height: 46px; line-height: 46px;
padding: 0 lh(.75); padding: 0 lh(.75);
text-indent: -9999px; text-indent: -9999px;
@include transition(background-color 0.75s linear 0s, opacity 0.75s linear 0s);
width: 14px; width: 14px;
background: url('../images/vcr.png') 15px 15px no-repeat; background: url('../images/vcr.png') 15px 15px no-repeat;
outline: 0; outline: 0;
...@@ -118,7 +149,7 @@ div.video { ...@@ -118,7 +149,7 @@ div.video {
&.play { &.play {
background-position: 17px -114px; background-position: 17px -114px;
&:hover { &:hover, &:focus {
background-color: #444; background-color: #444;
} }
} }
...@@ -126,7 +157,7 @@ div.video { ...@@ -126,7 +157,7 @@ div.video {
&.pause { &.pause {
background-position: 16px -50px; background-position: 16px -50px;
&:hover { &:hover, &:focus {
background-color: #444; background-color: #444;
} }
} }
...@@ -213,7 +244,7 @@ div.video { ...@@ -213,7 +244,7 @@ div.video {
// fix for now // fix for now
ol.video_speeds { ol.video_speeds {
box-shadow: inset 1px 0 0 #555, 0 3px 0 #444; box-shadow: inset 1px 0 0 #555, 0 4px 0 #444;
@include transition(none); @include transition(none);
background-color: #444; background-color: #444;
border: 1px solid #000; border: 1px solid #000;
...@@ -221,7 +252,7 @@ div.video { ...@@ -221,7 +252,7 @@ div.video {
display: none; display: none;
opacity: 0.0; opacity: 0.0;
position: absolute; position: absolute;
width: 133px; width: 131px;
z-index: 10; z-index: 10;
li { li {
...@@ -268,12 +299,15 @@ div.video { ...@@ -268,12 +299,15 @@ div.video {
&.muted { &.muted {
&>a { &>a {
background: url('../images/mute.png') 10px center no-repeat; background-image: url('../images/mute.png');
} }
} }
> a { > a {
background: url('../images/volume.png') 10px center no-repeat; background-image: url('../images/volume.png');
background-position: 10px center;
background-repeat: no-repeat;
border-right: 1px solid #000; border-right: 1px solid #000;
box-shadow: 1px 0 0 #555, inset 1px 0 0 #555; box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
@include clearfix(); @include clearfix();
...@@ -350,7 +384,7 @@ div.video { ...@@ -350,7 +384,7 @@ div.video {
@include transition(none); @include transition(none);
width: 30px; width: 30px;
&:hover { &:hover, &:active, &:focus {
background-color: #444; background-color: #444;
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
...@@ -362,7 +396,7 @@ div.video { ...@@ -362,7 +396,7 @@ div.video {
border-right: 1px solid #000; border-right: 1px solid #000;
box-shadow: 1px 0 0 #555, inset 1px 0 0 #555; box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
color: #797979; color: #797979;
display: block; display: none;
float: left; float: left;
line-height: 46px; //height of play pause buttons line-height: 46px; //height of play pause buttons
margin-left: 0; margin-left: 0;
...@@ -371,7 +405,7 @@ div.video { ...@@ -371,7 +405,7 @@ div.video {
@include transition(none); @include transition(none);
width: 30px; width: 30px;
&:hover { &:hover, &:focus {
background-color: #444; background-color: #444;
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
...@@ -387,8 +421,6 @@ div.video { ...@@ -387,8 +421,6 @@ div.video {
a.hide-subtitles { a.hide-subtitles {
background: url('../images/cc.png') center no-repeat; background: url('../images/cc.png') center no-repeat;
color: #797979;
display: block;
float: left; float: left;
font-weight: 800; font-weight: 800;
line-height: 46px; //height of play pause buttons line-height: 46px; //height of play pause buttons
...@@ -401,7 +433,7 @@ div.video { ...@@ -401,7 +433,7 @@ div.video {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
width: 30px; width: 30px;
&:hover { &:hover, &:focus {
background-color: #444; background-color: #444;
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
...@@ -410,6 +442,8 @@ div.video { ...@@ -410,6 +442,8 @@ div.video {
&.off { &.off {
opacity: 0.7; opacity: 0.7;
} }
color: #797979;
} }
} }
} }
...@@ -420,15 +454,10 @@ div.video { ...@@ -420,15 +454,10 @@ div.video {
} }
div.slider { div.slider {
height: 14px; @include transform(scaleY(1) translate3d(0, 0, 0));
margin-top: -7px;
a.ui-slider-handle { a.ui-slider-handle {
border-radius: 20px; @include transform(scale(1) translate3d(-50%, -15%, 0));
height: 20px;
margin-left: -10px;
top: -4px;
width: 20px;
} }
} }
} }
...@@ -471,22 +500,47 @@ div.video { ...@@ -471,22 +500,47 @@ div.video {
article.video-wrapper { article.video-wrapper {
width: flex-grid(9,9); width: flex-grid(9,9);
background-color: inherit;
}
article.video-wrapper section.video-controls.html5 {
bottom: 0px;
left: 0px;
right: 0px;
position: absolute;
z-index: 1;
}
article.video-wrapper div.video-player-pre, article.video-wrapper div.video-player-post {
height: 0px;
} }
ol.subtitles { ol.subtitles {
width: 0; width: 0;
height: 0; height: 0;
}
ol.subtitles.html5 {
background-color: rgba(243, 243, 243, 0.8);
height: 100%;
position: absolute;
right: 0;
bottom: 0;
top: 0;
width: 275px;
padding: 0 20px;
z-index: 0;
} }
} }
&.fullscreen { &.video-fullscreen {
background: rgba(#000, .95); background: rgba(#000, .95);
border: 0; border: 0;
bottom: 0; bottom: 0;
height: 100%; height: 100%;
left: 0; left: 0;
margin: 0; margin: 0;
overflow: hidden;
padding: 0; padding: 0;
position: fixed; position: fixed;
top: 0; top: 0;
...@@ -501,12 +555,22 @@ div.video { ...@@ -501,12 +555,22 @@ div.video {
} }
} }
article.video-wrapper div.video-player-pre, article.video-wrapper div.video-player-post {
height: 0px;
}
article.video-wrapper {
position: static;
}
div.tc-wrapper { div.tc-wrapper {
@include clearfix; @include clearfix;
display: table; display: table;
width: 100%; width: 100%;
height: 100%; height: 100%;
position: static;
article.video-wrapper { article.video-wrapper {
width: 100%; width: 100%;
display: table-cell; display: table-cell;
...@@ -536,7 +600,7 @@ div.video { ...@@ -536,7 +600,7 @@ div.video {
background: rgba(#000, .8); background: rgba(#000, .8);
bottom: 0; bottom: 0;
height: 100%; height: 100%;
max-height: 100%; max-height: 460px;
max-width: flex-grid(3); max-width: flex-grid(3);
padding: lh(); padding: lh();
position: fixed; position: fixed;
......
& {
margin-bottom: 30px;
}
div.videoalpha {
@include clearfix();
background: #f3f3f3;
display: block;
margin: 0 -12px;
padding: 12px;
border-radius: 5px;
div.tc-wrapper {
position: relative;
@include clearfix;
}
article.video-wrapper {
float: left;
margin-right: flex-gutter(9);
width: flex-grid(6, 9);
background-color: black;
position: relative;
div.video-player-pre {
height: 50px;
background-color: black;
}
div.video-player-post {
height: 50px;
background-color: black;
}
section.video-player {
height: 0;
overflow: hidden;
padding-bottom: 56.25%;
position: relative;
object, iframe {
border: none;
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
}
section.video-controls {
@include clearfix();
background: #333;
border: 1px solid #000;
border-top: 0;
color: #ccc;
position: relative;
&:hover {
ul, div {
opacity: 1.0;
}
}
div.slider {
@include clearfix();
background: #c2c2c2;
border: 1px solid #000;
border-radius: 0;
border-top: 1px solid #000;
box-shadow: inset 0 1px 0 #eee, 0 1px 0 #555;
position: absolute;
z-index: 1;
bottom: 100%;
left: 0;
right: 0;
height: 14px;
margin-left: -1px;
margin-right: -1px;
-webkit-transition: -webkit-transform 0.7s ease-in-out;
-moz-transition: -moz-transform 0.7s ease-in-out;
-ms-transition: -ms-transform 0.7s ease-in-out;
transition: transform 0.7s ease-in-out;
@include transform(scaleY(0.5) translate3d(0, 50%, 0));
div.ui-widget-header {
background: #777;
box-shadow: inset 0 1px 0 #999;
}
a.ui-slider-handle {
background: $pink url(../images/slider-handle.png) center center no-repeat;
background-size: 50%;
border: 1px solid darken($pink, 20%);
border-radius: 50%;
box-shadow: inset 0 1px 0 lighten($pink, 10%);
cursor: pointer;
height: 20px;
margin-left: 0;
top: 0;
-webkit-transition: -webkit-transform 0.7s ease-in-out;
-moz-transition: -moz-transform 0.7s ease-in-out;
-ms-transition: -ms-transform 0.7s ease-in-out;
transition: transform 0.7s ease-in-out;
@include transform(scale(.7, 1.3) translate3d(-80%, -15%, 0));
width: 20px;
&:focus, &:hover {
background-color: lighten($pink, 10%);
outline: none;
}
}
}
ul.vcr {
float: left;
list-style: none;
margin: 0 lh() 0 0;
padding: 0;
li {
float: left;
margin-bottom: 0;
a {
border-bottom: none;
border-right: 1px solid #000;
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;
outline: 0;
&:focus {
outline: 0;
}
&:empty {
height: 46px;
background: url('../images/vcr.png') 15px 15px no-repeat;
}
&.play {
background-position: 17px -114px;
&:hover, &:focus {
background-color: #444;
}
}
&.pause {
background-position: 16px -50px;
&:hover, &:focus {
background-color: #444;
}
}
}
div.vidtime {
padding-left: lh(.75);
font-weight: bold;
line-height: 46px; //height of play pause buttons
padding-left: lh(.75);
-webkit-font-smoothing: antialiased;
}
}
}
div.secondary-controls {
float: right;
div.speeds {
float: left;
position: relative;
&.open {
&>a {
background: url('../images/open-arrow.png') 10px center no-repeat;
}
ol.video_speeds {
display: block;
opacity: 1.0;
padding: 0;
margin: 0;
list-style: none;
}
}
&>a {
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;
@include clearfix();
color: #fff;
cursor: pointer;
display: block;
line-height: 46px; //height of play pause buttons
margin-right: 0;
padding-left: 15px;
position: relative;
@include transition(none);
-webkit-font-smoothing: antialiased;
width: 116px;
outline: 0;
&:focus {
outline: 0;
}
h3 {
color: #999;
float: left;
font-size: em(14);
font-weight: normal;
letter-spacing: 1px;
padding: 0 lh(.25) 0 lh(.5);
line-height: 46px;
text-transform: uppercase;
}
p.active {
float: left;
font-weight: bold;
margin-bottom: 0;
padding: 0 lh(.5) 0 0;
line-height: 46px;
color: #fff;
}
&:hover, &:active, &:focus {
opacity: 1.0;
background-color: #444;
}
}
// fix for now
ol.video_speeds {
box-shadow: inset 1px 0 0 #555, 0 4px 0 #444;
@include transition(none);
background-color: #444;
border: 1px solid #000;
bottom: 46px;
display: none;
opacity: 0.0;
position: absolute;
width: 131px;
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;
}
}
&.active {
font-weight: bold;
}
&:last-child {
box-shadow: none;
border-bottom: 0;
margin-top: 0;
}
}
}
}
div.volume {
float: left;
position: relative;
&.open {
.volume-slider-container {
display: block;
opacity: 1.0;
}
}
&.muted {
&>a {
background-image: url('../images/mute.png');
}
}
> a {
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;
@include clearfix();
color: #fff;
cursor: pointer;
display: block;
height: 46px;
margin-right: 0;
padding-left: 15px;
position: relative;
@include transition(none);
-webkit-font-smoothing: antialiased;
width: 30px;
&:hover, &:active, &:focus {
background-color: #444;
}
}
.volume-slider-container {
box-shadow: inset 1px 0 0 #555, 0 3px 0 #444;
@include transition(none);
background-color: #444;
border: 1px solid #000;
bottom: 46px;
display: none;
opacity: 0.0;
position: absolute;
width: 45px;
height: 125px;
margin-left: -1px;
z-index: 10;
.volume-slider {
height: 100px;
border: 0;
width: 5px;
margin: 14px auto;
background: #666;
border: 1px solid #000;
box-shadow: 0 1px 0 #333;
a.ui-slider-handle {
background: $pink url(../images/slider-handle.png) center center no-repeat;
background-size: 50%;
border: 1px solid darken($pink, 20%);
border-radius: 15px;
box-shadow: inset 0 1px 0 lighten($pink, 10%);
cursor: pointer;
height: 15px;
left: -6px;
@include transition(height 2.0s ease-in-out 0s, width 2.0s ease-in-out 0s);
width: 15px;
}
.ui-slider-range {
background: #ddd;
}
}
}
}
a.add-fullscreen {
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;
float: left;
line-height: 46px; //height of play pause buttons
margin-left: 0;
padding: 0 lh(.5);
text-indent: -9999px;
@include transition(none);
width: 30px;
&:hover, &:active, &:focus {
background-color: #444;
color: #fff;
text-decoration: none;
}
}
a.quality_control {
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;
display: none;
float: left;
line-height: 46px; //height of play pause buttons
margin-left: 0;
padding: 0 lh(.5);
text-indent: -9999px;
@include transition(none);
width: 30px;
&:hover, &:focus {
background-color: #444;
color: #fff;
text-decoration: none;
}
&.active {
background-color: #F44;
color: #0ff;
text-decoration: none;
}
}
a.hide-subtitles {
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.0;
padding: 0 lh(.5);
position: relative;
text-indent: -9999px;
@include transition(none);
-webkit-font-smoothing: antialiased;
width: 30px;
&:hover, &:focus {
background-color: #444;
color: #fff;
text-decoration: none;
}
&.off {
opacity: 0.7;
}
color: #797979;
}
}
}
&:hover section.video-controls {
ul, div {
opacity: 1.0;
}
div.slider {
@include transform(scaleY(1) translate3d(0, 0, 0));
a.ui-slider-handle {
@include transform(scale(1) translate3d(-50%, -15%, 0));
}
}
}
}
ol.subtitles {
padding-left: 0;
float: left;
max-height: 460px;
overflow: auto;
width: flex-grid(3, 9);
margin: 0;
font-size: 14px;
list-style: none;
li {
border: 0;
color: #666;
cursor: pointer;
margin-bottom: 8px;
padding: 0;
line-height: lh();
&.current {
color: #333;
font-weight: 700;
}
&:hover {
color: $blue;
}
&:empty {
margin-bottom: 0px;
}
}
}
&.closed {
article.video-wrapper {
width: flex-grid(9,9);
background-color: inherit;
}
article.video-wrapper section.video-controls.html5 {
bottom: 0px;
left: 0px;
right: 0px;
position: absolute;
z-index: 1;
}
article.video-wrapper div.video-player-pre, article.video-wrapper div.video-player-post {
height: 0px;
}
ol.subtitles {
width: 0;
height: 0;
}
ol.subtitles.html5 {
background-color: rgba(243, 243, 243, 0.8);
height: 100%;
position: absolute;
right: 0;
bottom: 0;
top: 0;
width: 275px;
padding: 0 20px;
z-index: 0;
}
}
&.video-fullscreen {
background: rgba(#000, .95);
border: 0;
bottom: 0;
height: 100%;
left: 0;
margin: 0;
padding: 0;
position: fixed;
top: 0;
width: 100%;
z-index: 999;
vertical-align: middle;
&.closed {
ol.subtitles {
right: -(flex-grid(4));
width: auto;
}
}
article.video-wrapper div.video-player-pre, article.video-wrapper div.video-player-post {
height: 0px;
}
article.video-wrapper {
position: static;
}
div.tc-wrapper {
@include clearfix;
display: table;
width: 100%;
height: 100%;
position: static;
article.video-wrapper {
width: 100%;
display: table-cell;
vertical-align: middle;
float: none;
}
object, iframe {
bottom: 0;
height: 100%;
left: 0;
overflow: hidden;
position: fixed;
top: 0;
}
section.video-controls {
bottom: 0;
left: 0;
position: absolute;
width: 100%;
z-index: 9999;
}
}
ol.subtitles {
background: rgba(#000, .8);
bottom: 0;
height: 100%;
max-height: 460px;
max-width: flex-grid(3);
padding: lh();
position: fixed;
right: 0;
top: 0;
@include transition(none);
li {
color: #aaa;
&.current {
color: #fff;
}
}
}
}
}
<div class="course-content"> <div class="course-content">
<div id="video_example"> <div id="video_example">
<div id="example"> <div id="example">
<div id="video_id" class="video" <div
data-youtube-id-0-75="7tqY6eQzVhE" id="video_id"
data-youtube-id-1-0="cogebirgzzM" class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="true" data-show-captions="true"
data-start="" data-start=""
data-end="" data-end=""
data-caption-asset-path="/static/subs/"> data-caption-asset-path="/static/subs/"
data-autoplay="False"
>
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<div class="video-player-pre"></div>
<section class="video-player"> <section class="video-player">
<div id="id"></div> <div id="id"></div>
</section> </section>
<section class="video-controls"></section> <div class="video-player-post"></div>
<section class="video-controls">
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#" title="Play"></a></li>
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
</a>
<ol class="video_speeds"></ol>
</div>
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</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>
</div>
</section>
</article> </article>
<ol class="subtitles"><li></li></ol>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div id="example"> <div id="example">
<div <div
id="video_id" id="video_id"
class="videoalpha" class="video"
data-show-captions="true" data-show-captions="true"
data-start="" data-start=""
data-end="" data-end=""
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div id="example"> <div id="example">
<div <div
id="video_id" id="video_id"
class="videoalpha" class="video"
data-show-captions="true" data-show-captions="true"
data-start="" data-start=""
data-end="" data-end=""
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div id="example"> <div id="example">
<div <div
id="video_id" id="video_id"
class="videoalpha" class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM" data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="false" data-show-captions="false"
data-start="" data-start=""
......
<div class="course-content">
<div id="video_example">
<div id="example">
<div
id="video_id"
class="videoalpha"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="true"
data-start=""
data-end=""
data-caption-asset-path="/static/subs/"
data-autoplay="False"
>
<div class="tc-wrapper">
<article class="video-wrapper">
<div class="video-player-pre"></div>
<section class="video-player">
<div id="id"></div>
</section>
<div class="video-player-post"></div>
<section class="video-controls">
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#" title="Play"></a></li>
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
</a>
<ol class="video_speeds"></ol>
</div>
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</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>
</div>
</section>
</article>
<ol class="subtitles"><li></li></ol>
</div>
</div>
</div>
</div>
</div>
\ No newline at end of file
*.js *.js
# Tests for videoalpha are written in pure JavaScript. # Tests for video are written in pure JavaScript.
!videoalpha/*.js !video/*.js
...@@ -111,34 +111,18 @@ jasmine.stubYoutubePlayer = -> ...@@ -111,34 +111,18 @@ jasmine.stubYoutubePlayer = ->
obj['getAvailablePlaybackRates'] = jasmine.createSpy('getAvailablePlaybackRates').andReturn [0.75, 1.0, 1.25, 1.5] obj['getAvailablePlaybackRates'] = jasmine.createSpy('getAvailablePlaybackRates').andReturn [0.75, 1.0, 1.25, 1.5]
obj obj
jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) -> jasmine.stubVideoPlayer = (context, enableParts, html5=false) ->
enableParts = [enableParts] unless $.isArray(enableParts)
suite = context.suite
currentPartName = suite.description while suite = suite.parentSuite
enableParts.push currentPartName
loadFixtures 'video.html'
jasmine.stubRequests()
YT.Player = undefined
videosDefinition = '0.75:7tqY6eQzVhE,1.0:cogebirgzzM'
context.video = new Video '#example', videosDefinition
jasmine.stubYoutubePlayer()
if createPlayer
return new VideoPlayer(video: context.video)
jasmine.stubVideoPlayerAlpha = (context, enableParts, html5=false) ->
console.log('stubVideoPlayerAlpha called')
suite = context.suite suite = context.suite
currentPartName = suite.description while suite = suite.parentSuite currentPartName = suite.description while suite = suite.parentSuite
if html5 == false if html5 == false
loadFixtures 'videoalpha.html' loadFixtures 'video.html'
else else
loadFixtures 'videoalpha_html5.html' loadFixtures 'video_html5.html'
jasmine.stubRequests() jasmine.stubRequests()
YT.Player = undefined YT.Player = undefined
window.OldVideoPlayerAlpha = undefined window.OldVideoPlayer = undefined
jasmine.stubYoutubePlayer() jasmine.stubYoutubePlayer()
return new VideoAlpha '#example', '.75:7tqY6eQzVhE,1.0:cogebirgzzM' return new Video '#example', '.75:7tqY6eQzVhE,1.0:cogebirgzzM'
# Stub jQuery.cookie # Stub jQuery.cookie
......
describe 'VideoCaption', ->
beforeEach ->
spyOn(VideoCaption.prototype, 'fetchCaption').andCallThrough()
spyOn($, 'ajaxWithPrefix').andCallThrough()
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
afterEach ->
YT.Player = undefined
$.fn.scrollTo.reset()
$('.subtitles').remove()
describe 'constructor', ->
describe 'always', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@caption = @player.caption
it 'set the youtube id', ->
expect(@caption.youtubeId).toEqual 'cogebirgzzM'
it 'create the caption element', ->
expect($('.video')).toContain 'ol.subtitles'
it 'add caption control to video player', ->
expect($('.video')).toContain 'a.hide-subtitles'
it 'fetch the caption', ->
expect(@caption.loaded).toBeTruthy()
expect(@caption.fetchCaption).toHaveBeenCalled()
expect($.ajaxWithPrefix).toHaveBeenCalledWith
url: @caption.captionURL()
notifyOnError: false
success: jasmine.any(Function)
it 'bind window resize event', ->
expect($(window)).toHandleWith 'resize', @caption.resize
it 'bind the hide caption button', ->
expect($('.hide-subtitles')).toHandleWith 'click', @caption.toggle
it 'bind the mouse movement', ->
expect($('.subtitles')).toHandleWith 'mouseover', @caption.onMouseEnter
expect($('.subtitles')).toHandleWith 'mouseout', @caption.onMouseLeave
expect($('.subtitles')).toHandleWith 'mousemove', @caption.onMovement
expect($('.subtitles')).toHandleWith 'mousewheel', @caption.onMovement
expect($('.subtitles')).toHandleWith 'DOMMouseScroll', @caption.onMovement
describe 'when on a non touch-based device', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@caption = @player.caption
it 'render the caption', ->
captionsData = jasmine.stubbedCaption
$('.subtitles li[data-index]').each (index, link) =>
expect($(link)).toHaveData 'index', index
expect($(link)).toHaveData 'start', captionsData.start[index]
expect($(link)).toHaveText captionsData.text[index]
it 'add a padding element to caption', ->
expect($('.subtitles li:first')).toBe '.spacing'
expect($('.subtitles li:last')).toBe '.spacing'
it 'bind all the caption link', ->
$('.subtitles li[data-index]').each (index, link) =>
expect($(link)).toHandleWith 'click', @caption.seekPlayer
it 'set rendered to true', ->
expect(@caption.rendered).toBeTruthy()
describe 'when on a touch-based device', ->
beforeEach ->
window.onTouchBasedDevice.andReturn true
@player = jasmine.stubVideoPlayer @
@caption = @player.caption
it 'show explaination message', ->
expect($('.subtitles li')).toHaveHtml "Caption will be displayed when you start playing the video."
it 'does not set rendered to true', ->
expect(@caption.rendered).toBeFalsy()
describe 'mouse movement', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@caption = @player.caption
window.setTimeout.andReturn(100)
spyOn window, 'clearTimeout'
describe 'when cursor is outside of the caption box', ->
beforeEach ->
$(window).trigger jQuery.Event 'mousemove'
it 'does not set freezing timeout', ->
expect(@caption.frozen).toBeFalsy()
describe 'when cursor is in the caption box', ->
beforeEach ->
$('.subtitles').trigger jQuery.Event 'mouseenter'
it 'set the freezing timeout', ->
expect(@caption.frozen).toEqual 100
describe 'when the cursor is moving', ->
beforeEach ->
$('.subtitles').trigger jQuery.Event 'mousemove'
it 'reset the freezing timeout', ->
expect(window.clearTimeout).toHaveBeenCalledWith 100
describe 'when the mouse is scrolling', ->
beforeEach ->
$('.subtitles').trigger jQuery.Event 'mousewheel'
it 'reset the freezing timeout', ->
expect(window.clearTimeout).toHaveBeenCalledWith 100
describe 'when cursor is moving out of the caption box', ->
beforeEach ->
@caption.frozen = 100
$.fn.scrollTo.reset()
describe 'always', ->
beforeEach ->
$('.subtitles').trigger jQuery.Event 'mouseout'
it 'reset the freezing timeout', ->
expect(window.clearTimeout).toHaveBeenCalledWith 100
it 'unfreeze the caption', ->
expect(@caption.frozen).toBeNull()
describe 'when the player is playing', ->
beforeEach ->
@caption.playing = true
$('.subtitles li[data-index]:first').addClass 'current'
$('.subtitles').trigger jQuery.Event 'mouseout'
it 'scroll the caption', ->
expect($.fn.scrollTo).toHaveBeenCalled()
describe 'when the player is not playing', ->
beforeEach ->
@caption.playing = false
$('.subtitles').trigger jQuery.Event 'mouseout'
it 'does not scroll the caption', ->
expect($.fn.scrollTo).not.toHaveBeenCalled()
describe 'search', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@caption = @player.caption
it 'return a correct caption index', ->
expect(@caption.search(0)).toEqual 0
expect(@caption.search(9999)).toEqual 2
expect(@caption.search(10000)).toEqual 2
expect(@caption.search(15000)).toEqual 3
expect(@caption.search(30000)).toEqual 7
expect(@caption.search(30001)).toEqual 7
describe 'play', ->
describe 'when the caption was not rendered', ->
beforeEach ->
window.onTouchBasedDevice.andReturn true
@player = jasmine.stubVideoPlayer @
@caption = @player.caption
@caption.play()
it 'render the caption', ->
captionsData = jasmine.stubbedCaption
$('.subtitles li[data-index]').each (index, link) =>
expect($(link)).toHaveData 'index', index
expect($(link)).toHaveData 'start', captionsData.start[index]
expect($(link)).toHaveText captionsData.text[index]
it 'add a padding element to caption', ->
expect($('.subtitles li:first')).toBe '.spacing'
expect($('.subtitles li:last')).toBe '.spacing'
it 'bind all the caption link', ->
$('.subtitles li[data-index]').each (index, link) =>
expect($(link)).toHandleWith 'click', @caption.seekPlayer
it 'set rendered to true', ->
expect(@caption.rendered).toBeTruthy()
it 'set playing to true', ->
expect(@caption.playing).toBeTruthy()
describe 'pause', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@caption = @player.caption
@caption.playing = true
@caption.pause()
it 'set playing to false', ->
expect(@caption.playing).toBeFalsy()
describe 'updatePlayTime', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@caption = @player.caption
describe 'when the video speed is 1.0x', ->
beforeEach ->
@caption.currentSpeed = '1.0'
@caption.updatePlayTime 25.000
it 'search the caption based on time', ->
expect(@caption.currentIndex).toEqual 5
describe 'when the video speed is not 1.0x', ->
beforeEach ->
@caption.currentSpeed = '0.75'
@caption.updatePlayTime 25.000
it 'search the caption based on 1.0x speed', ->
expect(@caption.currentIndex).toEqual 3
describe 'when the index is not the same', ->
beforeEach ->
@caption.currentIndex = 1
$('.subtitles li[data-index=1]').addClass 'current'
@caption.updatePlayTime 25.000
it 'deactivate the previous caption', ->
expect($('.subtitles li[data-index=1]')).not.toHaveClass 'current'
it 'activate new caption', ->
expect($('.subtitles li[data-index=5]')).toHaveClass 'current'
it 'save new index', ->
expect(@caption.currentIndex).toEqual 5
it 'scroll caption to new position', ->
expect($.fn.scrollTo).toHaveBeenCalled()
describe 'when the index is the same', ->
beforeEach ->
@caption.currentIndex = 1
$('.subtitles li[data-index=3]').addClass 'current'
@caption.updatePlayTime 15.000
it 'does not change current subtitle', ->
expect($('.subtitles li[data-index=3]')).toHaveClass 'current'
describe 'resize', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@caption = @player.caption
$('.subtitles li[data-index=1]').addClass 'current'
@caption.resize()
it 'set the height of caption container', ->
expect(parseInt($('.subtitles').css('maxHeight'))).toBeCloseTo $('.video-wrapper').height(), 2
it 'set the height of caption spacing', ->
expect(Math.abs(parseInt($('.subtitles .spacing:first').css('height')) - @caption.topSpacingHeight())).toBeLessThan 1
expect(Math.abs(parseInt($('.subtitles .spacing:last').css('height')) - @caption.bottomSpacingHeight())).toBeLessThan 1
it 'scroll caption to new position', ->
expect($.fn.scrollTo).toHaveBeenCalled()
describe 'scrollCaption', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@caption = @player.caption
describe 'when frozen', ->
beforeEach ->
@caption.frozen = true
$('.subtitles li[data-index=1]').addClass 'current'
@caption.scrollCaption()
it 'does not scroll the caption', ->
expect($.fn.scrollTo).not.toHaveBeenCalled()
describe 'when not frozen', ->
beforeEach ->
@caption.frozen = false
describe 'when there is no current caption', ->
beforeEach ->
@caption.scrollCaption()
it 'does not scroll the caption', ->
expect($.fn.scrollTo).not.toHaveBeenCalled()
describe 'when there is a current caption', ->
beforeEach ->
$('.subtitles li[data-index=1]').addClass 'current'
@caption.scrollCaption()
it 'scroll to current caption', ->
expect($.fn.scrollTo).toHaveBeenCalledWith $('.subtitles .current:first', @caption.el),
offset: - ($('.video-wrapper').height() / 2 - $('.subtitles .current:first').height() / 2)
describe 'seekPlayer', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@caption = @player.caption
@time = null
$(@caption).bind 'seek', (event, time) => @time = time
describe 'when the video speed is 1.0x', ->
beforeEach ->
@caption.currentSpeed = '1.0'
$('.subtitles li[data-start="27900"]').trigger('click')
it 'trigger seek event with the correct time', ->
expect(@time).toEqual 28.000
describe 'when the video speed is not 1.0x', ->
beforeEach ->
@caption.currentSpeed = '0.75'
$('.subtitles li[data-start="27900"]').trigger('click')
it 'trigger seek event with the correct time', ->
expect(@time).toEqual 37.000
describe 'toggle', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@caption = @player.caption
$('.subtitles li[data-index=1]').addClass 'current'
describe 'when the caption is visible', ->
beforeEach ->
@caption.el.removeClass 'closed'
@caption.toggle jQuery.Event('click')
it 'hide the caption', ->
expect(@caption.el).toHaveClass 'closed'
describe 'when the caption is hidden', ->
beforeEach ->
@caption.el.addClass 'closed'
@caption.toggle jQuery.Event('click')
it 'show the caption', ->
expect(@caption.el).not.toHaveClass 'closed'
it 'scroll the caption', ->
expect($.fn.scrollTo).toHaveBeenCalled()
describe 'VideoControl', ->
beforeEach ->
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
loadFixtures 'video.html'
$('.video-controls').html ''
describe 'constructor', ->
it 'render the video controls', ->
@control = new window.VideoControl(el: $('.video-controls'))
expect($('.video-controls')).toContain
['.slider', 'ul.vcr', 'a.play', '.vidtime', '.add-fullscreen'].join(',')
expect($('.video-controls').find('.vidtime')).toHaveText '0:00 / 0:00'
it 'bind the playback button', ->
@control = new window.VideoControl(el: $('.video-controls'))
expect($('.video_control')).toHandleWith 'click', @control.togglePlayback
describe 'when on a touch based device', ->
beforeEach ->
window.onTouchBasedDevice.andReturn true
@control = new window.VideoControl(el: $('.video-controls'))
it 'does not add the play class to video control', ->
expect($('.video_control')).not.toHaveClass 'play'
expect($('.video_control')).not.toHaveHtml 'Play'
describe 'when on a non-touch based device', ->
beforeEach ->
@control = new window.VideoControl(el: $('.video-controls'))
it 'add the play class to video control', ->
expect($('.video_control')).toHaveClass 'play'
expect($('.video_control')).toHaveHtml 'Play'
describe 'play', ->
beforeEach ->
@control = new window.VideoControl(el: $('.video-controls'))
@control.play()
it 'switch playback button to play state', ->
expect($('.video_control')).not.toHaveClass 'play'
expect($('.video_control')).toHaveClass 'pause'
expect($('.video_control')).toHaveHtml 'Pause'
describe 'pause', ->
beforeEach ->
@control = new window.VideoControl(el: $('.video-controls'))
@control.pause()
it 'switch playback button to pause state', ->
expect($('.video_control')).not.toHaveClass 'pause'
expect($('.video_control')).toHaveClass 'play'
expect($('.video_control')).toHaveHtml 'Play'
describe 'togglePlayback', ->
beforeEach ->
@control = new window.VideoControl(el: $('.video-controls'))
describe 'when the control does not have play or pause class', ->
beforeEach ->
$('.video_control').removeClass('play').removeClass('pause')
describe 'when the video is playing', ->
beforeEach ->
$('.video_control').addClass('play')
spyOnEvent @control, 'pause'
@control.togglePlayback jQuery.Event('click')
it 'does not trigger the pause event', ->
expect('pause').not.toHaveBeenTriggeredOn @control
describe 'when the video is paused', ->
beforeEach ->
$('.video_control').addClass('pause')
spyOnEvent @control, 'play'
@control.togglePlayback jQuery.Event('click')
it 'does not trigger the play event', ->
expect('play').not.toHaveBeenTriggeredOn @control
describe 'when the video is playing', ->
beforeEach ->
spyOnEvent @control, 'pause'
$('.video_control').addClass 'pause'
@control.togglePlayback jQuery.Event('click')
it 'trigger the pause event', ->
expect('pause').toHaveBeenTriggeredOn @control
describe 'when the video is paused', ->
beforeEach ->
spyOnEvent @control, 'play'
$('.video_control').addClass 'play'
@control.togglePlayback jQuery.Event('click')
it 'trigger the play event', ->
expect('play').toHaveBeenTriggeredOn @control
describe 'VideoPlayer', ->
beforeEach ->
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
# It tries to call methods of VideoProgressSlider on Spy
for part in ['VideoCaption', 'VideoSpeedControl', 'VideoVolumeControl', 'VideoProgressSlider', 'VideoControl']
spyOn(window[part].prototype, 'initialize').andCallThrough()
jasmine.stubVideoPlayer @, [], false
afterEach ->
YT.Player = undefined
describe 'constructor', ->
beforeEach ->
spyOn YT, 'Player'
$.fn.qtip.andCallFake ->
$(this).data('qtip', true)
$('.video').append $('<div class="add-fullscreen" /><div class="hide-subtitles" />')
describe 'always', ->
beforeEach ->
@player = new VideoPlayer video: @video
it 'instanticate current time to zero', ->
expect(@player.currentTime).toEqual 0
it 'set the element', ->
expect(@player.el).toHaveId 'video_id'
it 'create video control', ->
expect(window.VideoControl.prototype.initialize).toHaveBeenCalled()
expect(@player.control).toBeDefined()
expect(@player.control.el).toBe $('.video-controls', @player.el)
it 'create video caption', ->
expect(window.VideoCaption.prototype.initialize).toHaveBeenCalled()
expect(@player.caption).toBeDefined()
expect(@player.caption.el).toBe @player.el
expect(@player.caption.youtubeId).toEqual 'cogebirgzzM'
expect(@player.caption.currentSpeed).toEqual '1.0'
expect(@player.caption.captionAssetPath).toEqual '/static/subs/'
it 'create video speed control', ->
expect(window.VideoSpeedControl.prototype.initialize).toHaveBeenCalled()
expect(@player.speedControl).toBeDefined()
expect(@player.speedControl.el).toBe $('.secondary-controls', @player.el)
expect(@player.speedControl.speeds).toEqual ['0.75', '1.0']
expect(@player.speedControl.currentSpeed).toEqual '1.0'
it 'create video progress slider', ->
expect(window.VideoSpeedControl.prototype.initialize).toHaveBeenCalled()
expect(@player.progressSlider).toBeDefined()
expect(@player.progressSlider.el).toBe $('.slider', @player.el)
it 'create Youtube player', ->
expect(YT.Player).toHaveBeenCalledWith('id', {
playerVars:
controls: 0
wmode: 'transparent'
rel: 0
showinfo: 0
enablejsapi: 1
modestbranding: 1
videoId: 'cogebirgzzM'
events:
onReady: @player.onReady
onStateChange: @player.onStateChange
onPlaybackQualityChange: @player.onPlaybackQualityChange
})
it 'bind to video control play event', ->
expect($(@player.control)).toHandleWith 'play', @player.play
it 'bind to video control pause event', ->
expect($(@player.control)).toHandleWith 'pause', @player.pause
it 'bind to video caption seek event', ->
expect($(@player.caption)).toHandleWith 'seek', @player.onSeek
it 'bind to video speed control speedChange event', ->
expect($(@player.speedControl)).toHandleWith 'speedChange', @player.onSpeedChange
it 'bind to video progress slider seek event', ->
expect($(@player.progressSlider)).toHandleWith 'seek', @player.onSeek
it 'bind to video volume control volumeChange event', ->
expect($(@player.volumeControl)).toHandleWith 'volumeChange', @player.onVolumeChange
it 'bind to key press', ->
expect($(document.documentElement)).toHandleWith 'keyup', @player.bindExitFullScreen
it 'bind to fullscreen switching button', ->
expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen
describe 'when not on a touch based device', ->
beforeEach ->
$('.add-fullscreen, .hide-subtitles').removeData 'qtip'
@player = new VideoPlayer video: @video
it 'add the tooltip to fullscreen and subtitle button', ->
expect($('.add-fullscreen')).toHaveData 'qtip'
expect($('.hide-subtitles')).toHaveData 'qtip'
it 'create video volume control', ->
expect(window.VideoVolumeControl.prototype.initialize).toHaveBeenCalled()
expect(@player.volumeControl).toBeDefined()
expect(@player.volumeControl.el).toBe $('.secondary-controls', @player.el)
describe 'when on a touch based device', ->
beforeEach ->
window.onTouchBasedDevice.andReturn true
$('.add-fullscreen, .hide-subtitles').removeData 'qtip'
@player = new VideoPlayer video: @video
it 'does not add the tooltip to fullscreen and subtitle button', ->
expect($('.add-fullscreen')).not.toHaveData 'qtip'
expect($('.hide-subtitles')).not.toHaveData 'qtip'
it 'does not create video volume control', ->
expect(window.VideoVolumeControl.prototype.initialize).not.toHaveBeenCalled()
expect(@player.volumeControl).not.toBeDefined()
describe 'onReady', ->
beforeEach ->
@video.embed()
@player = @video.player
spyOnEvent @player, 'ready'
spyOnEvent @player, 'updatePlayTime'
@player.onReady()
describe 'when not on a touch based device', ->
beforeEach ->
spyOn @player, 'play'
@player.onReady()
it 'autoplay the first video', ->
expect(@player.play).toHaveBeenCalled()
describe 'when on a touch based device', ->
beforeEach ->
window.onTouchBasedDevice.andReturn true
spyOn @player, 'play'
@player.onReady()
it 'does not autoplay the first video', ->
expect(@player.play).not.toHaveBeenCalled()
describe 'onStateChange', ->
beforeEach ->
@player = new VideoPlayer video: @video
describe 'when the video is unstarted', ->
beforeEach ->
spyOn @player.control, 'pause'
@player.caption.pause = jasmine.createSpy('VideoCaption.pause')
@player.onStateChange data: YT.PlayerState.UNSTARTED
it 'pause the video control', ->
expect(@player.control.pause).toHaveBeenCalled()
it 'pause the video caption', ->
expect(@player.caption.pause).toHaveBeenCalled()
describe 'when the video is playing', ->
beforeEach ->
@anotherPlayer = jasmine.createSpyObj 'AnotherPlayer', ['pauseVideo']
window.player = @anotherPlayer
spyOn @video, 'log'
spyOn(window, 'setInterval').andReturn 100
spyOn @player.control, 'play'
@player.caption.play = jasmine.createSpy('VideoCaption.play')
@player.progressSlider.play = jasmine.createSpy('VideoProgressSlider.play')
@player.player.getVideoEmbedCode.andReturn 'embedCode'
@player.onStateChange data: YT.PlayerState.PLAYING
it 'log the play_video event', ->
expect(@video.log).toHaveBeenCalledWith 'play_video'
it 'pause other video player', ->
expect(@anotherPlayer.pauseVideo).toHaveBeenCalled()
it 'set current video player as active player', ->
expect(window.player).toEqual @player.player
it 'set update interval', ->
expect(window.setInterval).toHaveBeenCalledWith @player.update, 200
expect(@player.player.interval).toEqual 100
it 'play the video control', ->
expect(@player.control.play).toHaveBeenCalled()
it 'play the video caption', ->
expect(@player.caption.play).toHaveBeenCalled()
it 'play the video progress slider', ->
expect(@player.progressSlider.play).toHaveBeenCalled()
describe 'when the video is paused', ->
beforeEach ->
@player = new VideoPlayer video: @video
window.player = @player.player
spyOn @video, 'log'
spyOn window, 'clearInterval'
spyOn @player.control, 'pause'
@player.caption.pause = jasmine.createSpy('VideoCaption.pause')
@player.player.interval = 100
@player.player.getVideoEmbedCode.andReturn 'embedCode'
@player.onStateChange data: YT.PlayerState.PAUSED
it 'log the pause_video event', ->
expect(@video.log).toHaveBeenCalledWith 'pause_video'
it 'set current video player as inactive', ->
expect(window.player).toBeNull()
it 'clear update interval', ->
expect(window.clearInterval).toHaveBeenCalledWith 100
expect(@player.player.interval).toBeNull()
it 'pause the video control', ->
expect(@player.control.pause).toHaveBeenCalled()
it 'pause the video caption', ->
expect(@player.caption.pause).toHaveBeenCalled()
describe 'when the video is ended', ->
beforeEach ->
spyOn @player.control, 'pause'
@player.caption.pause = jasmine.createSpy('VideoCaption.pause')
@player.onStateChange data: YT.PlayerState.ENDED
it 'pause the video control', ->
expect(@player.control.pause).toHaveBeenCalled()
it 'pause the video caption', ->
expect(@player.caption.pause).toHaveBeenCalled()
describe 'onSeek', ->
beforeEach ->
@player = new VideoPlayer video: @video
spyOn window, 'clearInterval'
@player.player.interval = 100
spyOn @player, 'updatePlayTime'
@player.onSeek {}, 60
it 'seek the player', ->
expect(@player.player.seekTo).toHaveBeenCalledWith 60, true
it 'call updatePlayTime on player', ->
expect(@player.updatePlayTime).toHaveBeenCalledWith 60
describe 'when the player is playing', ->
beforeEach ->
@player.player.getPlayerState.andReturn YT.PlayerState.PLAYING
@player.onSeek {}, 60
it 'reset the update interval', ->
expect(window.clearInterval).toHaveBeenCalledWith 100
describe 'when the player is not playing', ->
beforeEach ->
@player.player.getPlayerState.andReturn YT.PlayerState.PAUSED
@player.onSeek {}, 60
it 'set the current time', ->
expect(@player.currentTime).toEqual 60
describe 'onSpeedChange', ->
beforeEach ->
@player = new VideoPlayer video: @video
@player.currentTime = 60
spyOn @player, 'updatePlayTime'
spyOn(@video, 'setSpeed').andCallThrough()
describe 'always', ->
beforeEach ->
@player.onSpeedChange {}, '0.75'
it 'convert the current time to the new speed', ->
expect(@player.currentTime).toEqual '80.000'
it 'set video speed to the new speed', ->
expect(@video.setSpeed).toHaveBeenCalledWith '0.75'
it 'tell video caption that the speed has changed', ->
expect(@player.caption.currentSpeed).toEqual '0.75'
describe 'when the video is playing', ->
beforeEach ->
@player.player.getPlayerState.andReturn YT.PlayerState.PLAYING
@player.onSpeedChange {}, '0.75'
it 'load the video', ->
expect(@player.player.loadVideoById).toHaveBeenCalledWith '7tqY6eQzVhE', '80.000'
it 'trigger updatePlayTime event', ->
expect(@player.updatePlayTime).toHaveBeenCalledWith '80.000'
describe 'when the video is not playing', ->
beforeEach ->
@player.player.getPlayerState.andReturn YT.PlayerState.PAUSED
@player.onSpeedChange {}, '0.75'
it 'cue the video', ->
expect(@player.player.cueVideoById).toHaveBeenCalledWith '7tqY6eQzVhE', '80.000'
it 'trigger updatePlayTime event', ->
expect(@player.updatePlayTime).toHaveBeenCalledWith '80.000'
describe 'onVolumeChange', ->
beforeEach ->
@player = new VideoPlayer video: @video
@player.onVolumeChange undefined, 60
it 'set the volume on player', ->
expect(@player.player.setVolume).toHaveBeenCalledWith 60
describe 'update', ->
beforeEach ->
@player = new VideoPlayer video: @video
spyOn @player, 'updatePlayTime'
describe 'when the current time is unavailable from the player', ->
beforeEach ->
@player.player.getCurrentTime.andReturn undefined
@player.update()
it 'does not trigger updatePlayTime event', ->
expect(@player.updatePlayTime).not.toHaveBeenCalled()
describe 'when the current time is available from the player', ->
beforeEach ->
@player.player.getCurrentTime.andReturn 60
@player.update()
it 'trigger updatePlayTime event', ->
expect(@player.updatePlayTime).toHaveBeenCalledWith(60)
describe 'updatePlayTime', ->
beforeEach ->
@player = new VideoPlayer video: @video
spyOn(@video, 'getDuration').andReturn 1800
@player.caption.updatePlayTime = jasmine.createSpy('VideoCaption.updatePlayTime')
@player.progressSlider.updatePlayTime = jasmine.createSpy('VideoProgressSlider.updatePlayTime')
@player.updatePlayTime 60
it 'update the video playback time', ->
expect($('.vidtime')).toHaveHtml '1:00 / 30:00'
it 'update the playback time on caption', ->
expect(@player.caption.updatePlayTime).toHaveBeenCalledWith 60
it 'update the playback time on progress slider', ->
expect(@player.progressSlider.updatePlayTime).toHaveBeenCalledWith 60, 1800
describe 'toggleFullScreen', ->
beforeEach ->
@player = new VideoPlayer video: @video
@player.caption.resize = jasmine.createSpy('VideoCaption.resize')
describe 'when the video player is not full screen', ->
beforeEach ->
@player.el.removeClass 'fullscreen'
@player.toggleFullScreen(jQuery.Event("click"))
it 'replace the full screen button tooltip', ->
expect($('.add-fullscreen')).toHaveAttr 'title', 'Exit fill browser'
it 'add the fullscreen class', ->
expect(@player.el).toHaveClass 'fullscreen'
it 'tell VideoCaption to resize', ->
expect(@player.caption.resize).toHaveBeenCalled()
describe 'when the video player already full screen', ->
beforeEach ->
@player.el.addClass 'fullscreen'
@player.toggleFullScreen(jQuery.Event("click"))
it 'replace the full screen button tooltip', ->
expect($('.add-fullscreen')).toHaveAttr 'title', 'Fill browser'
it 'remove exit full screen button', ->
expect(@player.el).not.toContain 'a.exit'
it 'remove the fullscreen class', ->
expect(@player.el).not.toHaveClass 'fullscreen'
it 'tell VideoCaption to resize', ->
expect(@player.caption.resize).toHaveBeenCalled()
describe 'play', ->
beforeEach ->
@player = new VideoPlayer video: @video
describe 'when the player is not ready', ->
beforeEach ->
@player.player.playVideo = undefined
@player.play()
it 'does nothing', ->
expect(@player.player.playVideo).toBeUndefined()
describe 'when the player is ready', ->
beforeEach ->
@player.player.playVideo.andReturn true
@player.play()
it 'delegate to the Youtube player', ->
expect(@player.player.playVideo).toHaveBeenCalled()
describe 'isPlaying', ->
beforeEach ->
@player = new VideoPlayer video: @video
describe 'when the video is playing', ->
beforeEach ->
@player.player.getPlayerState.andReturn YT.PlayerState.PLAYING
it 'return true', ->
expect(@player.isPlaying()).toBeTruthy()
describe 'when the video is not playing', ->
beforeEach ->
@player.player.getPlayerState.andReturn YT.PlayerState.PAUSED
it 'return false', ->
expect(@player.isPlaying()).toBeFalsy()
describe 'pause', ->
beforeEach ->
@player = new VideoPlayer video: @video
@player.pause()
it 'delegate to the Youtube player', ->
expect(@player.player.pauseVideo).toHaveBeenCalled()
describe 'duration', ->
beforeEach ->
@player = new VideoPlayer video: @video
spyOn @video, 'getDuration'
@player.duration()
it 'delegate to the video', ->
expect(@video.getDuration).toHaveBeenCalled()
describe 'currentSpeed', ->
beforeEach ->
@player = new VideoPlayer video: @video
@video.speed = '3.0'
it 'delegate to the video', ->
expect(@player.currentSpeed()).toEqual '3.0'
describe 'volume', ->
beforeEach ->
@player = new VideoPlayer video: @video
@player.player.getVolume.andReturn 42
describe 'without value', ->
it 'return current volume', ->
expect(@player.volume()).toEqual 42
describe 'with value', ->
it 'set player volume', ->
@player.volume(60)
expect(@player.player.setVolume).toHaveBeenCalledWith(60)
describe 'VideoProgressSlider', ->
beforeEach ->
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
describe 'constructor', ->
describe 'on a non-touch based device', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
it 'build the slider', ->
expect(@progressSlider.slider).toBe '.slider'
expect($.fn.slider).toHaveBeenCalledWith
range: 'min'
change: @progressSlider.onChange
slide: @progressSlider.onSlide
stop: @progressSlider.onStop
it 'build the seek handle', ->
expect(@progressSlider.handle).toBe '.slider .ui-slider-handle'
expect($.fn.qtip).toHaveBeenCalledWith
content: "0:00"
position:
my: 'bottom center'
at: 'top center'
container: @progressSlider.handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
describe 'on a touch-based device', ->
beforeEach ->
window.onTouchBasedDevice.andReturn true
spyOn($.fn, 'slider').andCallThrough()
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
it 'does not build the slider', ->
expect(@progressSlider.slider).toBeUndefined
expect($.fn.slider).not.toHaveBeenCalled()
describe 'play', ->
beforeEach ->
spyOn(VideoProgressSlider.prototype, 'buildSlider').andCallThrough()
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
describe 'when the slider was already built', ->
beforeEach ->
@progressSlider.play()
it 'does not build the slider', ->
expect(@progressSlider.buildSlider.calls.length).toEqual 1
describe 'when the slider was not already built', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
@progressSlider.slider = null
@progressSlider.play()
it 'build the slider', ->
expect(@progressSlider.slider).toBe '.slider'
expect($.fn.slider).toHaveBeenCalledWith
range: 'min'
change: @progressSlider.onChange
slide: @progressSlider.onSlide
stop: @progressSlider.onStop
it 'build the seek handle', ->
expect(@progressSlider.handle).toBe '.ui-slider-handle'
expect($.fn.qtip).toHaveBeenCalledWith
content: "0:00"
position:
my: 'bottom center'
at: 'top center'
container: @progressSlider.handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
describe 'updatePlayTime', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
describe 'when frozen', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
@progressSlider.frozen = true
@progressSlider.updatePlayTime 20, 120
it 'does not update the slider', ->
expect($.fn.slider).not.toHaveBeenCalled()
describe 'when not frozen', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
@progressSlider.frozen = false
@progressSlider.updatePlayTime 20, 120
it 'update the max value of the slider', ->
expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 120
it 'update current value of the slider', ->
expect($.fn.slider).toHaveBeenCalledWith 'value', 20
describe 'onSlide', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
@time = null
$(@progressSlider).bind 'seek', (event, time) => @time = time
spyOnEvent @progressSlider, 'seek'
@progressSlider.onSlide {}, value: 20
it 'freeze the slider', ->
expect(@progressSlider.frozen).toBeTruthy()
it 'update the tooltip', ->
expect($.fn.qtip).toHaveBeenCalled()
it 'trigger seek event', ->
expect('seek').toHaveBeenTriggeredOn @progressSlider
expect(@time).toEqual 20
describe 'onChange', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
@progressSlider.onChange {}, value: 20
it 'update the tooltip', ->
expect($.fn.qtip).toHaveBeenCalled()
describe 'onStop', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
@time = null
$(@progressSlider).bind 'seek', (event, time) => @time = time
spyOnEvent @progressSlider, 'seek'
@progressSlider.onStop {}, value: 20
it 'freeze the slider', ->
expect(@progressSlider.frozen).toBeTruthy()
it 'trigger seek event', ->
expect('seek').toHaveBeenTriggeredOn @progressSlider
expect(@time).toEqual 20
it 'set timeout to unfreeze the slider', ->
expect(window.setTimeout).toHaveBeenCalledWith jasmine.any(Function), 200
window.setTimeout.mostRecentCall.args[0]()
expect(@progressSlider.frozen).toBeFalsy()
describe 'updateTooltip', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
@progressSlider.updateTooltip 90
it 'set the tooltip value', ->
expect($.fn.qtip).toHaveBeenCalledWith 'option', 'content.text', '1:30'
describe 'VideoSpeedControl', ->
beforeEach ->
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
jasmine.stubVideoPlayer @
$('.speeds').remove()
describe 'constructor', ->
describe 'always', ->
beforeEach ->
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
it 'add the video speed control to player', ->
secondaryControls = $('.secondary-controls')
li = secondaryControls.find('.video_speeds li')
expect(secondaryControls).toContain '.speeds'
expect(secondaryControls).toContain '.video_speeds'
expect(secondaryControls.find('p.active').text()).toBe '1.0x'
expect(li.filter('.active')).toHaveData 'speed', @speedControl.currentSpeed
expect(li.length).toBe @speedControl.speeds.length
$.each li.toArray().reverse(), (index, link) =>
expect($(link)).toHaveData 'speed', @speedControl.speeds[index]
expect($(link).find('a').text()).toBe @speedControl.speeds[index] + 'x'
it 'bind to change video speed link', ->
expect($('.video_speeds a')).toHandleWith 'click', @speedControl.changeVideoSpeed
describe 'when running on touch based device', ->
beforeEach ->
window.onTouchBasedDevice.andReturn true
$('.speeds').removeClass 'open'
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
it 'open the speed toggle on click', ->
$('.speeds').click()
expect($('.speeds')).toHaveClass 'open'
$('.speeds').click()
expect($('.speeds')).not.toHaveClass 'open'
describe 'when running on non-touch based device', ->
beforeEach ->
$('.speeds').removeClass 'open'
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
it 'open the speed toggle on hover', ->
$('.speeds').mouseenter()
expect($('.speeds')).toHaveClass 'open'
$('.speeds').mouseleave()
expect($('.speeds')).not.toHaveClass 'open'
it 'close the speed toggle on mouse out', ->
$('.speeds').mouseenter().mouseleave()
expect($('.speeds')).not.toHaveClass 'open'
it 'close the speed toggle on click', ->
$('.speeds').mouseenter().click()
expect($('.speeds')).not.toHaveClass 'open'
describe 'changeVideoSpeed', ->
beforeEach ->
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
@video.setSpeed '1.0'
describe 'when new speed is the same', ->
beforeEach ->
spyOnEvent @speedControl, 'speedChange'
$('li[data-speed="1.0"] a').click()
it 'does not trigger speedChange event', ->
expect('speedChange').not.toHaveBeenTriggeredOn @speedControl
describe 'when new speed is not the same', ->
beforeEach ->
@newSpeed = null
$(@speedControl).bind 'speedChange', (event, newSpeed) => @newSpeed = newSpeed
spyOnEvent @speedControl, 'speedChange'
$('li[data-speed="0.75"] a').click()
it 'trigger speedChange event', ->
expect('speedChange').toHaveBeenTriggeredOn @speedControl
expect(@newSpeed).toEqual 0.75
describe 'onSpeedChange', ->
beforeEach ->
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
$('li[data-speed="1.0"] a').addClass 'active'
@speedControl.setSpeed '0.75'
it 'set the new speed as active', ->
expect($('.video_speeds li[data-speed="1.0"]')).not.toHaveClass 'active'
expect($('.video_speeds li[data-speed="0.75"]')).toHaveClass 'active'
expect($('.speeds p.active')).toHaveHtml '0.75x'
describe 'VideoVolumeControl', ->
beforeEach ->
jasmine.stubVideoPlayer @
$('.volume').remove()
describe 'constructor', ->
beforeEach ->
spyOn($.fn, 'slider')
@volumeControl = new VideoVolumeControl el: $('.secondary-controls')
it 'initialize currentVolume to 100', ->
expect(@volumeControl.currentVolume).toEqual 100
it 'render the volume control', ->
expect($('.secondary-controls').html()).toContain """
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
"""
it 'create the slider', ->
expect($.fn.slider).toHaveBeenCalledWith
orientation: "vertical"
range: "min"
min: 0
max: 100
value: 100
change: @volumeControl.onChange
slide: @volumeControl.onChange
it 'bind the volume control', ->
expect($('.volume>a')).toHandleWith 'click', @volumeControl.toggleMute
expect($('.volume')).not.toHaveClass 'open'
$('.volume').mouseenter()
expect($('.volume')).toHaveClass 'open'
$('.volume').mouseleave()
expect($('.volume')).not.toHaveClass 'open'
describe 'onChange', ->
beforeEach ->
spyOnEvent @volumeControl, 'volumeChange'
@newVolume = undefined
@volumeControl = new VideoVolumeControl el: $('.secondary-controls')
$(@volumeControl).bind 'volumeChange', (event, volume) => @newVolume = volume
describe 'when the new volume is more than 0', ->
beforeEach ->
@volumeControl.onChange undefined, value: 60
it 'set the player volume', ->
expect(@newVolume).toEqual 60
it 'remote muted class', ->
expect($('.volume')).not.toHaveClass 'muted'
describe 'when the new volume is 0', ->
beforeEach ->
@volumeControl.onChange undefined, value: 0
it 'set the player volume', ->
expect(@newVolume).toEqual 0
it 'add muted class', ->
expect($('.volume')).toHaveClass 'muted'
describe 'toggleMute', ->
beforeEach ->
@newVolume = undefined
@volumeControl = new VideoVolumeControl el: $('.secondary-controls')
$(@volumeControl).bind 'volumeChange', (event, volume) => @newVolume = volume
describe 'when the current volume is more than 0', ->
beforeEach ->
@volumeControl.currentVolume = 60
@volumeControl.toggleMute()
it 'save the previous volume', ->
expect(@volumeControl.previousVolume).toEqual 60
it 'set the player volume', ->
expect(@newVolume).toEqual 0
describe 'when the current volume is 0', ->
beforeEach ->
@volumeControl.currentVolume = 0
@volumeControl.previousVolume = 60
@volumeControl.toggleMute()
it 'set the player volume to previous volume', ->
expect(@newVolume).toEqual 60
describe 'Video', ->
metadata = undefined
beforeEach ->
loadFixtures 'video.html'
jasmine.stubRequests()
@['7tqY6eQzVhE'] = '7tqY6eQzVhE'
@['cogebirgzzM'] = 'cogebirgzzM'
metadata =
'7tqY6eQzVhE':
id: @['7tqY6eQzVhE']
duration: 300
'cogebirgzzM':
id: @['cogebirgzzM']
duration: 200
afterEach ->
window.player = undefined
window.onYouTubePlayerAPIReady = undefined
describe 'constructor', ->
beforeEach ->
@stubVideoPlayer = jasmine.createSpy('VideoPlayer')
$.cookie.andReturn '0.75'
window.player = undefined
describe 'by default', ->
beforeEach ->
spyOn(window.Video.prototype, 'fetchMetadata').andCallFake ->
@metadata = metadata
@video = new Video '#example'
it 'reset the current video player', ->
expect(window.player).toBeNull()
it 'set the elements', ->
expect(@video.el).toBe '#video_id'
it 'parse the videos', ->
expect(@video.videos).toEqual
'0.75': @['7tqY6eQzVhE']
'1.0': @['cogebirgzzM']
it 'fetch the video metadata', ->
expect(@video.fetchMetadata).toHaveBeenCalled
expect(@video.metadata).toEqual metadata
it 'parse available video speeds', ->
expect(@video.speeds).toEqual ['0.75', '1.0']
it 'set current video speed via cookie', ->
expect(@video.speed).toEqual '0.75'
it 'store a reference for this video player in the element', ->
expect($('.video').data('video')).toEqual @video
describe 'when the Youtube API is already available', ->
beforeEach ->
@originalYT = window.YT
window.YT = { Player: true }
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video '#example'
afterEach ->
window.YT = @originalYT
it 'create the Video Player', ->
expect(window.VideoPlayer).toHaveBeenCalledWith(video: @video)
expect(@video.player).toEqual @stubVideoPlayer
describe 'when the Youtube API is not ready', ->
beforeEach ->
@originalYT = window.YT
window.YT = {}
@video = new Video '#example'
afterEach ->
window.YT = @originalYT
it 'set the callback on the window object', ->
expect(window.onYouTubePlayerAPIReady).toEqual jasmine.any(Function)
describe 'when the Youtube API becoming ready', ->
beforeEach ->
@originalYT = window.YT
window.YT = {}
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video '#example'
window.onYouTubePlayerAPIReady()
afterEach ->
window.YT = @originalYT
it 'create the Video Player for all video elements', ->
expect(window.VideoPlayer).toHaveBeenCalledWith(video: @video)
expect(@video.player).toEqual @stubVideoPlayer
describe 'youtubeId', ->
beforeEach ->
$.cookie.andReturn '1.0'
@video = new Video '#example'
describe 'with speed', ->
it 'return the video id for given speed', ->
expect(@video.youtubeId('0.75')).toEqual @['7tqY6eQzVhE']
expect(@video.youtubeId('1.0')).toEqual @['cogebirgzzM']
describe 'without speed', ->
it 'return the video id for current speed', ->
expect(@video.youtubeId()).toEqual @cogebirgzzM
describe 'setSpeed', ->
beforeEach ->
@video = new Video '#example'
describe 'when new speed is available', ->
beforeEach ->
@video.setSpeed '0.75'
it 'set new speed', ->
expect(@video.speed).toEqual '0.75'
it 'save setting for new speed', ->
expect($.cookie).toHaveBeenCalledWith 'video_speed', '0.75', expires: 3650, path: '/'
describe 'when new speed is not available', ->
beforeEach ->
@video.setSpeed '1.75'
it 'set speed to 1.0x', ->
expect(@video.speed).toEqual '1.0'
describe 'getDuration', ->
beforeEach ->
@video = new Video '#example'
it 'return duration for current video', ->
expect(@video.getDuration()).toEqual 200
describe 'log', ->
beforeEach ->
@video = new Video '#example'
@video.setSpeed '1.0'
spyOn Logger, 'log'
@video.player = { currentTime: 25 }
@video.log 'someEvent'
it 'call the logger with valid parameters', ->
expect(Logger.log).toHaveBeenCalledWith 'someEvent',
id: 'id'
code: @cogebirgzzM
currentTime: 25
speed: '1.0'
(function () { (function () {
xdescribe('VideoAlpha', function () { xdescribe('Video', function () {
var oldOTBD; var oldOTBD;
beforeEach(function () { beforeEach(function () {
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
}); });
afterEach(function () { afterEach(function () {
window.OldVideoPlayerAlpha = undefined; window.OldVideoPlayer = undefined;
window.onYouTubePlayerAPIReady = undefined; window.onYouTubePlayerAPIReady = undefined;
window.onHTML5PlayerAPIReady = undefined; window.onHTML5PlayerAPIReady = undefined;
$('source').remove(); $('source').remove();
...@@ -22,13 +22,13 @@ ...@@ -22,13 +22,13 @@
describe('constructor', function () { describe('constructor', function () {
describe('YT', function () { describe('YT', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('videoalpha.html'); loadFixtures('video.html');
$.cookie.andReturn('0.75'); $.cookie.andReturn('0.75');
}); });
describe('by default', function () { describe('by default', function () {
beforeEach(function () { beforeEach(function () {
this.state = new window.VideoAlpha('#example'); this.state = new window.Video('#example');
}); });
it('check videoType', function () { it('check videoType', function () {
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
}); });
it('reset the current video player', function () { it('reset the current video player', function () {
expect(window.OldVideoPlayerAlpha).toBeUndefined(); expect(window.OldVideoPlayer).toBeUndefined();
}); });
it('set the elements', function () { it('set the elements', function () {
...@@ -64,14 +64,14 @@ ...@@ -64,14 +64,14 @@
var state; var state;
beforeEach(function () { beforeEach(function () {
loadFixtures('videoalpha_html5.html'); loadFixtures('video_html5.html');
this.stubVideoPlayerAlpha = jasmine.createSpy('VideoPlayerAlpha'); this.stubVideoPlayer = jasmine.createSpy('VideoPlayer');
$.cookie.andReturn('0.75'); $.cookie.andReturn('0.75');
}); });
describe('by default', function () { describe('by default', function () {
beforeEach(function () { beforeEach(function () {
state = new window.VideoAlpha('#example'); state = new window.Video('#example');
}); });
afterEach(function () { afterEach(function () {
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
}); });
it('reset the current video player', function () { it('reset the current video player', function () {
expect(window.OldVideoPlayerAlpha).toBeUndefined(); expect(window.OldVideoPlayer).toBeUndefined();
}); });
it('set the elements', function () { it('set the elements', function () {
...@@ -104,8 +104,8 @@ ...@@ -104,8 +104,8 @@
it('parse the videos if subtitles do not exist', function () { it('parse the videos if subtitles do not exist', function () {
var sub = ''; var sub = '';
$('#example').find('.videoalpha').data('sub', ''); $('#example').find('.video').data('sub', '');
state = new window.VideoAlpha('#example'); state = new window.Video('#example');
expect(state.videos).toEqual({ expect(state.videos).toEqual({
'0.75': sub, '0.75': sub,
...@@ -142,7 +142,7 @@ ...@@ -142,7 +142,7 @@
// is required. // is required.
describe('HTML5 API is available', function () { describe('HTML5 API is available', function () {
beforeEach(function () { beforeEach(function () {
state = new VideoAlpha('#example'); state = new Video('#example');
}); });
afterEach(function () { afterEach(function () {
...@@ -158,9 +158,9 @@ ...@@ -158,9 +158,9 @@
describe('youtubeId', function () { describe('youtubeId', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('videoalpha.html'); loadFixtures('video.html');
$.cookie.andReturn('1.0'); $.cookie.andReturn('1.0');
state = new VideoAlpha('#example'); state = new Video('#example');
}); });
describe('with speed', function () { describe('with speed', function () {
...@@ -180,13 +180,13 @@ ...@@ -180,13 +180,13 @@
describe('setSpeed', function () { describe('setSpeed', function () {
describe('YT', function () { describe('YT', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('videoalpha.html'); loadFixtures('video.html');
state = new VideoAlpha('#example'); state = new Video('#example');
}); });
describe('when new speed is available', function () { describe('when new speed is available', function () {
beforeEach(function () { beforeEach(function () {
state.setSpeed('0.75'); state.setSpeed('0.75', true);
}); });
it('set new speed', function () { it('set new speed', function () {
...@@ -214,13 +214,13 @@ ...@@ -214,13 +214,13 @@
describe('HTML5', function () { describe('HTML5', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('videoalpha_html5.html'); loadFixtures('video_html5.html');
state = new VideoAlpha('#example'); state = new Video('#example');
}); });
describe('when new speed is available', function () { describe('when new speed is available', function () {
beforeEach(function () { beforeEach(function () {
state.setSpeed('0.75'); state.setSpeed('0.75', true);
}); });
it('set new speed', function () { it('set new speed', function () {
...@@ -249,8 +249,8 @@ ...@@ -249,8 +249,8 @@
describe('getDuration', function () { describe('getDuration', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('videoalpha.html'); loadFixtures('video.html');
state = new VideoAlpha('#example'); state = new Video('#example');
}); });
it('return duration for current video', function () { it('return duration for current video', function () {
...@@ -260,8 +260,8 @@ ...@@ -260,8 +260,8 @@
describe('log', function () { describe('log', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('videoalpha_html5.html'); loadFixtures('video_html5.html');
state = new VideoAlpha('#example'); state = new Video('#example');
spyOn(Logger, 'log'); spyOn(Logger, 'log');
state.videoPlayer.log('someEvent', { state.videoPlayer.log('someEvent', {
currentTime: 25, currentTime: 25,
......
(function () { (function () {
xdescribe('VideoAlpha HTML5Video', function () { xdescribe('Video HTML5Video', function () {
var state, player, oldOTBD, playbackRates = [0.75, 1.0, 1.25, 1.5]; var state, player, oldOTBD, playbackRates = [0.75, 1.0, 1.25, 1.5];
function initialize() { function initialize() {
loadFixtures('videoalpha_html5.html'); loadFixtures('video_html5.html');
state = new VideoAlpha('#example'); state = new Video('#example');
player = state.videoPlayer.player; player = state.videoPlayer.player;
} }
......
(function() { (function() {
xdescribe('VideoCaptionAlpha', function() { xdescribe('VideoCaption', function() {
var state, videoPlayer, videoCaption, videoSpeedControl, oldOTBD; var state, videoPlayer, videoCaption, videoSpeedControl, oldOTBD;
function initialize() { function initialize() {
loadFixtures('videoalpha_all.html'); loadFixtures('video_all.html');
state = new VideoAlpha('#example'); state = new Video('#example');
videoPlayer = state.videoPlayer; videoPlayer = state.videoPlayer;
videoCaption = state.videoCaption; videoCaption = state.videoCaption;
videoSpeedControl = state.videoSpeedControl; videoSpeedControl = state.videoSpeedControl;
...@@ -33,11 +33,11 @@ ...@@ -33,11 +33,11 @@
}); });
it('create the caption element', function() { it('create the caption element', function() {
expect($('.videoalpha')).toContain('ol.subtitles'); expect($('.video')).toContain('ol.subtitles');
}); });
it('add caption control to video player', function() { it('add caption control to video player', function() {
expect($('.videoalpha')).toContain('a.hide-subtitles'); expect($('.video')).toContain('a.hide-subtitles');
}); });
it('fetch the caption', function() { it('fetch the caption', function() {
......
(function() { (function() {
xdescribe('VideoControlAlpha', function() { xdescribe('VideoControl', function() {
var state, videoControl, oldOTBD; var state, videoControl, oldOTBD;
function initialize() { function initialize() {
loadFixtures('videoalpha_all.html'); loadFixtures('video_all.html');
state = new VideoAlpha('#example'); state = new Video('#example');
videoControl = state.videoControl; videoControl = state.videoControl;
} }
......
(function() { (function() {
xdescribe('VideoPlayerAlpha', function() { xdescribe('VideoPlayer', function() {
var state, videoPlayer, player, videoControl, videoCaption, videoProgressSlider, videoSpeedControl, videoVolumeControl, oldOTBD; var state, videoPlayer, player, videoControl, videoCaption, videoProgressSlider, videoSpeedControl, videoVolumeControl, oldOTBD;
function initialize(fixture) { function initialize(fixture) {
if (typeof fixture === 'undefined') { if (typeof fixture === 'undefined') {
loadFixtures('videoalpha_all.html'); loadFixtures('video_all.html');
} else { } else {
loadFixtures(fixture); loadFixtures(fixture);
} }
state = new VideoAlpha('#example'); state = new Video('#example');
videoPlayer = state.videoPlayer; videoPlayer = state.videoPlayer;
player = videoPlayer.player; player = videoPlayer.player;
videoControl = state.videoControl; videoControl = state.videoControl;
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
} }
function initializeYouTube() { function initializeYouTube() {
initialize('videoalpha.html'); initialize('video.html');
} }
beforeEach(function () { beforeEach(function () {
...@@ -71,9 +71,9 @@ ...@@ -71,9 +71,9 @@
expect(videoProgressSlider.el).toHaveClass('slider'); expect(videoProgressSlider.el).toHaveClass('slider');
}); });
// All the toHandleWith() expect tests are not necessary for this version of Video Alpha. // All the toHandleWith() expect tests are not necessary for this version of Video.
// jQuery event system is not used to trigger and invoke methods. This is an artifact from // jQuery event system is not used to trigger and invoke methods. This is an artifact from
// previous version of Video Alpha. // previous version of Video.
}); });
it('create Youtube player', function() { it('create Youtube player', function() {
......
(function() { (function() {
xdescribe('VideoProgressSliderAlpha', function() { xdescribe('VideoProgressSlider', function() {
var state, videoPlayer, videoProgressSlider, oldOTBD; var state, videoPlayer, videoProgressSlider, oldOTBD;
function initialize() { function initialize() {
loadFixtures('videoalpha_all.html'); loadFixtures('video_all.html');
state = new VideoAlpha('#example'); state = new Video('#example');
videoPlayer = state.videoPlayer; videoPlayer = state.videoPlayer;
videoProgressSlider = state.videoProgressSlider; videoProgressSlider = state.videoProgressSlider;
} }
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
expect(videoProgressSlider.slider).toBeUndefined(); expect(videoProgressSlider.slider).toBeUndefined();
// We can't expect $.fn.slider not to have been called, // We can't expect $.fn.slider not to have been called,
// because sliders are used in other parts of VideoAlpha. // because sliders are used in other parts of Video.
}); });
}); });
}); });
......
(function() { (function() {
xdescribe('VideoQualityControlAlpha', function() { xdescribe('VideoQualityControl', function() {
var state, videoControl, videoQualityControl, oldOTBD; var state, videoControl, videoQualityControl, oldOTBD;
function initialize() { function initialize() {
loadFixtures('videoalpha.html'); loadFixtures('video.html');
state = new VideoAlpha('#example'); state = new Video('#example');
videoControl = state.videoControl; videoControl = state.videoControl;
videoQualityControl = state.videoQualityControl; videoQualityControl = state.videoQualityControl;
} }
......
(function() { (function() {
xdescribe('VideoSpeedControlAlpha', function() { xdescribe('VideoSpeedControl', function() {
var state, videoPlayer, videoControl, videoSpeedControl; var state, videoPlayer, videoControl, videoSpeedControl;
function initialize() { function initialize() {
loadFixtures('videoalpha_all.html'); loadFixtures('video_all.html');
state = new VideoAlpha('#example'); state = new Video('#example');
videoPlayer = state.videoPlayer; videoPlayer = state.videoPlayer;
videoControl = state.videoControl; videoControl = state.videoControl;
videoSpeedControl = state.videoSpeedControl; videoSpeedControl = state.videoSpeedControl;
......
(function() { (function() {
xdescribe('VideoVolumeControlAlpha', function() { xdescribe('VideoVolumeControl', function() {
var state, videoControl, videoVolumeControl, oldOTBD; var state, videoControl, videoVolumeControl, oldOTBD;
function initialize() { function initialize() {
loadFixtures('videoalpha_all.html'); loadFixtures('video_all.html');
state = new VideoAlpha('#example'); state = new Video('#example');
videoControl = state.videoControl; videoControl = state.videoControl;
videoVolumeControl = state.videoVolumeControl; videoVolumeControl = state.videoVolumeControl;
} }
......
...@@ -4,5 +4,5 @@ ...@@ -4,5 +4,5 @@
*.js *.js
# Videoalpha are written in pure JavaScript. # Video are written in pure JavaScript.
!videoalpha/*.js !video/*.js
\ No newline at end of file \ No newline at end of file
...@@ -88,7 +88,7 @@ class @Sequence ...@@ -88,7 +88,7 @@ class @Sequence
$.postWithPrefix modx_full_url, position: new_position $.postWithPrefix modx_full_url, position: new_position
# On Sequence change, fire custom event "sequence:change" on element. # On Sequence change, fire custom event "sequence:change" on element.
# Added for aborting video bufferization, see ../videoalpha/10_main.js # Added for aborting video bufferization, see ../video/10_main.js
@el.trigger "sequence:change" @el.trigger "sequence:change"
@mark_active new_position @mark_active new_position
@$('#seq_content').html @contents.eq(new_position - 1).text() @$('#seq_content').html @contents.eq(new_position - 1).text()
......
...@@ -12,8 +12,8 @@ ...@@ -12,8 +12,8 @@
(function (requirejs, require, define) { (function (requirejs, require, define) {
define( define(
'videoalpha/01_initialize.js', 'video/01_initialize.js',
['videoalpha/03_video_player.js'], ['video/03_video_player.js'],
function (VideoPlayer) { function (VideoPlayer) {
if (typeof(window.gettext) == "undefined") { if (typeof(window.gettext) == "undefined") {
...@@ -25,8 +25,8 @@ function (VideoPlayer) { ...@@ -25,8 +25,8 @@ function (VideoPlayer) {
* *
* Initialize module exports this function. * Initialize module exports this function.
* *
* @param {Object} state A place for all properties, and methods of Video Alpha. * @param {Object} state A place for all properties, and methods of Video.
* @param {DOM element} element Container of the entire Video Alpha DOM element. * @param {DOM element} element Container of the entire Video DOM element.
*/ */
return function (state, element) { return function (state, element) {
_makeFunctionsPublic(state); _makeFunctionsPublic(state);
...@@ -44,7 +44,7 @@ function (VideoPlayer) { ...@@ -44,7 +44,7 @@ function (VideoPlayer) {
* Functions which will be accessible via 'state' object. When called, these functions will get the 'state' * Functions which will be accessible via 'state' object. When called, these functions will get the 'state'
* object as a context. * object as a context.
* *
* @param {Object} state A place for all properties, and methods of Video Alpha. * @param {Object} state A place for all properties, and methods of Video.
*/ */
function _makeFunctionsPublic(state) { function _makeFunctionsPublic(state) {
state.setSpeed = _.bind(setSpeed, state); state.setSpeed = _.bind(setSpeed, state);
...@@ -70,7 +70,7 @@ function (VideoPlayer) { ...@@ -70,7 +70,7 @@ function (VideoPlayer) {
state.isFullScreen = false; state.isFullScreen = false;
// The parent element of the video, and the ID. // The parent element of the video, and the ID.
state.el = $(element).find('.videoalpha'); state.el = $(element).find('.video');
state.id = state.el.attr('id').replace(/video_/, ''); state.id = state.el.attr('id').replace(/video_/, '');
// We store all settings passed to us by the server in one place. These are "read only", so don't // We store all settings passed to us by the server in one place. These are "read only", so don't
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
(function (requirejs, require, define) { (function (requirejs, require, define) {
define( define(
'videoalpha/02_html5_video.js', 'video/02_html5_video.js',
[], [],
function () { function () {
var HTML5Video = {}; var HTML5Video = {};
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
// VideoPlayer module. // VideoPlayer module.
define( define(
'videoalpha/03_video_player.js', 'video/03_video_player.js',
['videoalpha/02_html5_video.js'], ['video/02_html5_video.js'],
function (HTML5Video) { function (HTML5Video) {
// VideoPlayer() function - what this module "exports". // VideoPlayer() function - what this module "exports".
...@@ -359,7 +359,7 @@ function (HTML5Video) { ...@@ -359,7 +359,7 @@ function (HTML5Video) {
this.videoPlayer.player.setPlaybackRate(this.speed); this.videoPlayer.player.setPlaybackRate(this.speed);
} }
if (!onTouchBasedDevice() && $('.videoalpha:first').data('autoplay') === 'True') { if (!onTouchBasedDevice() && $('.video:first').data('autoplay') === 'True') {
this.videoPlayer.play(); this.videoPlayer.play();
} }
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// VideoControl module. // VideoControl module.
define( define(
'videoalpha/04_video_control.js', 'video/04_video_control.js',
[], [],
function () { function () {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// VideoQualityControl module. // VideoQualityControl module.
define( define(
'videoalpha/05_video_quality_control.js', 'video/05_video_quality_control.js',
[], [],
function () { function () {
......
...@@ -9,7 +9,7 @@ mind, or whether to act, and in acting, to live." ...@@ -9,7 +9,7 @@ mind, or whether to act, and in acting, to live."
// VideoProgressSlider module. // VideoProgressSlider module.
define( define(
'videoalpha/06_video_progress_slider.js', 'video/06_video_progress_slider.js',
[], [],
function () { function () {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// VideoVolumeControl module. // VideoVolumeControl module.
define( define(
'videoalpha/07_video_volume_control.js', 'video/07_video_volume_control.js',
[], [],
function () { function () {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// VideoSpeedControl module. // VideoSpeedControl module.
define( define(
'videoalpha/08_video_speed_control.js', 'video/08_video_speed_control.js',
[], [],
function () { function () {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// VideoCaption module. // VideoCaption module.
define( define(
'videoalpha/09_video_caption.js', 'video/09_video_caption.js',
[], [],
function () { function () {
......
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
// Main module. // Main module.
require( require(
[ [
'videoalpha/01_initialize.js', 'video/01_initialize.js',
'videoalpha/04_video_control.js', 'video/04_video_control.js',
'videoalpha/05_video_quality_control.js', 'video/05_video_quality_control.js',
'videoalpha/06_video_progress_slider.js', 'video/06_video_progress_slider.js',
'videoalpha/07_video_volume_control.js', 'video/07_video_volume_control.js',
'videoalpha/08_video_speed_control.js', 'video/08_video_speed_control.js',
'videoalpha/09_video_caption.js' 'video/09_video_caption.js'
], ],
function ( function (
Initialize, Initialize,
...@@ -31,7 +31,7 @@ function ( ...@@ -31,7 +31,7 @@ function (
// afterwards (expecting the DOM elements to be present) must be stopped by hand. // afterwards (expecting the DOM elements to be present) must be stopped by hand.
previousState = null; previousState = null;
window.VideoAlpha = function (element) { window.Video = function (element) {
var state; var state;
// Stop bufferization of previous video on sequence change. // Stop bufferization of previous video on sequence change.
...@@ -64,7 +64,7 @@ function ( ...@@ -64,7 +64,7 @@ function (
// Because the 'state' object is only available inside this closure, we will also make // Because the 'state' object is only available inside this closure, we will also make
// it available to the caller by returning it. This is necessary so that we can test // it available to the caller by returning it. This is necessary so that we can test
// VideoAlpha with Jasmine. // Video with Jasmine.
return state; return state;
}; };
}); });
......
class @Video
constructor: (element) ->
@el = $(element).find('.video')
@id = @el.attr('id').replace(/video_/, '')
@start = @el.data('start')
@end = @el.data('end')
@caption_asset_path = @el.data('caption-asset-path')
@show_captions = @el.data('show-captions')
window.player = null
@el = $("#video_#{@id}")
@parseVideos()
@fetchMetadata()
@parseSpeed()
$("#video_#{@id}").data('video', this).addClass('video-load-complete')
@hide_captions = $.cookie('hide_captions') == 'true' or (not @show_captions)
if YT.Player
@embed()
else
window.onYouTubePlayerAPIReady = =>
@el.each ->
$(this).data('video').embed()
youtubeId: (speed)->
@videos[speed || @speed]
parseVideos: (videos) ->
@videos = {}
if @el.data('youtube-id-0-75')
@videos['0.75'] = @el.data('youtube-id-0-75')
if @el.data('youtube-id-1-0')
@videos['1.0'] = @el.data('youtube-id-1-0')
if @el.data('youtube-id-1-25')
@videos['1.25'] = @el.data('youtube-id-1-25')
if @el.data('youtube-id-1-5')
@videos['1.50'] = @el.data('youtube-id-1-5')
parseSpeed: ->
@setSpeed($.cookie('video_speed'))
@speeds = ($.map @videos, (url, speed) -> speed).sort()
setSpeed: (newSpeed) ->
if @videos[newSpeed] != undefined
@speed = newSpeed
$.cookie('video_speed', "#{newSpeed}", expires: 3650, path: '/')
else
@speed = '1.0'
embed: ->
@player = new VideoPlayer video: this
fetchMetadata: (url) ->
@metadata = {}
$.each @videos, (speed, url) =>
$.get "https://gdata.youtube.com/feeds/api/videos/#{url}?v=2&alt=jsonc", ((data) => @metadata[data.data.id] = data.data) , 'jsonp'
getDuration: ->
@metadata[@youtubeId()].duration
log: (eventName) ->
Logger.log eventName,
id: @id
code: @youtubeId()
currentTime: @player.currentTime
speed: @speed
class @Subview
constructor: (options) ->
$.each options, (key, value) =>
@[key] = value
@initialize()
@render()
@bind()
$: (selector) ->
$(selector, @el)
initialize: ->
render: ->
bind: ->
class @VideoCaption extends Subview
initialize: ->
@loaded = false
bind: ->
$(window).bind('resize', @resize)
@$('.hide-subtitles').click @toggle
@$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave)
.mousemove(@onMovement).bind('mousewheel', @onMovement)
.bind('DOMMouseScroll', @onMovement)
captionURL: ->
"#{@captionAssetPath}#{@youtubeId}.srt.sjson"
render: ->
# TODO: make it so you can have a video with no captions.
#@$('.video-wrapper').after """
# <ol class="subtitles"><li>Attempting to load captions...</li></ol>
# """
@$('.video-wrapper').after """
<ol class="subtitles"></ol>
"""
@$('.video-controls .secondary-controls').append """
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
"""#"
@$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5
@fetchCaption()
fetchCaption: ->
$.ajaxWithPrefix
url: @captionURL()
notifyOnError: false
success: (captions) =>
@captions = captions.text
@start = captions.start
@loaded = true
if onTouchBasedDevice()
$('.subtitles').html "<li>Caption will be displayed when you start playing the video.</li>"
else
@renderCaption()
renderCaption: ->
container = $('<ol>')
$.each @captions, (index, text) =>
container.append $('<li>').html(text).attr
'data-index': index
'data-start': @start[index]
@$('.subtitles').html(container.html())
@$('.subtitles li[data-index]').click @seekPlayer
# prepend and append an empty <li> for cosmetic reason
@$('.subtitles').prepend($('<li class="spacing">').height(@topSpacingHeight()))
.append($('<li class="spacing">').height(@bottomSpacingHeight()))
@rendered = true
search: (time) ->
if @loaded
min = 0
max = @start.length - 1
while min < max
index = Math.ceil((max + min) / 2)
if time < @start[index]
max = index - 1
if time >= @start[index]
min = index
return min
play: ->
if @loaded
@renderCaption() unless @rendered
@playing = true
pause: ->
if @loaded
@playing = false
updatePlayTime: (time) ->
if @loaded
# This 250ms offset is required to match the video speed
time = Math.round(Time.convert(time, @currentSpeed, '1.0') * 1000 + 250)
newIndex = @search time
if newIndex != undefined && @currentIndex != newIndex
if @currentIndex
@$(".subtitles li.current").removeClass('current')
@$(".subtitles li[data-index='#{newIndex}']").addClass('current')
@currentIndex = newIndex
@scrollCaption()
resize: =>
@$('.subtitles').css maxHeight: @captionHeight()
@$('.subtitles .spacing:first').height(@topSpacingHeight())
@$('.subtitles .spacing:last').height(@bottomSpacingHeight())
@scrollCaption()
onMouseEnter: =>
clearTimeout @frozen if @frozen
@frozen = setTimeout @onMouseLeave, 10000
onMovement: =>
@onMouseEnter()
onMouseLeave: =>
clearTimeout @frozen if @frozen
@frozen = null
@scrollCaption() if @playing
scrollCaption: ->
if !@frozen && @$('.subtitles .current:first').length
@$('.subtitles').scrollTo @$('.subtitles .current:first'),
offset: - @calculateOffset(@$('.subtitles .current:first'))
seekPlayer: (event) =>
event.preventDefault()
time = Math.round(Time.convert($(event.target).data('start'), '1.0', @currentSpeed) / 1000)
$(@).trigger('seek', time)
calculateOffset: (element) ->
@captionHeight() / 2 - element.height() / 2
topSpacingHeight: ->
@calculateOffset(@$('.subtitles li:not(.spacing):first'))
bottomSpacingHeight: ->
@calculateOffset(@$('.subtitles li:not(.spacing):last'))
toggle: (event) =>
event.preventDefault()
if @el.hasClass('closed') # Captions are "closed" e.g. turned off
@hideCaptions(false)
else # Captions are on
@hideCaptions(true)
hideCaptions: (hide_captions) =>
if hide_captions
@$('.hide-subtitles').attr('title', 'Turn on captions')
@el.addClass('closed')
else
@$('.hide-subtitles').attr('title', 'Turn off captions')
@el.removeClass('closed')
@scrollCaption()
$.cookie('hide_captions', hide_captions, expires: 3650, path: '/')
captionHeight: ->
if @el.hasClass('fullscreen')
$(window).height() - @$('.video-controls').height()
else
@$('.video-wrapper').height()
class @VideoControl extends Subview
bind: ->
@$('.video_control').click @togglePlayback
render: ->
@el.append """
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#"></a></li>
<li>
<div class="vidtime">0:00 / 0:00</div>
</li>
</ul>
<div class="secondary-controls">
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
</div>
</div>
"""#"
unless onTouchBasedDevice()
@$('.video_control').addClass('play').html('Play')
play: ->
@$('.video_control').removeClass('play').addClass('pause').html('Pause')
pause: ->
@$('.video_control').removeClass('pause').addClass('play').html('Play')
togglePlayback: (event) =>
event.preventDefault()
if @$('.video_control').hasClass('play')
$(@).trigger('play')
else if @$('.video_control').hasClass('pause')
$(@).trigger('pause')
class @VideoPlayer extends Subview
initialize: ->
# Define a missing constant of Youtube API
YT.PlayerState.UNSTARTED = -1
@currentTime = 0
@el = $("#video_#{@video.id}")
bind: ->
$(@control).bind('play', @play)
.bind('pause', @pause)
$(@qualityControl).bind('changeQuality', @handlePlaybackQualityChange)
$(@caption).bind('seek', @onSeek)
$(@speedControl).bind('speedChange', @onSpeedChange)
$(@progressSlider).bind('seek', @onSeek)
if @volumeControl
$(@volumeControl).bind('volumeChange', @onVolumeChange)
$(document.documentElement).keyup @bindExitFullScreen
@$('.add-fullscreen').click @toggleFullScreen
@addToolTip() unless onTouchBasedDevice()
bindExitFullScreen: (event) =>
if @el.hasClass('fullscreen') && event.keyCode == 27
@toggleFullScreen(event)
render: ->
@control = new VideoControl el: @$('.video-controls')
@qualityControl = new VideoQualityControl el: @$('.secondary-controls')
@caption = new VideoCaption
el: @el
youtubeId: @video.youtubeId('1.0')
currentSpeed: @currentSpeed()
captionAssetPath: @video.caption_asset_path
unless onTouchBasedDevice()
@volumeControl = new VideoVolumeControl el: @$('.secondary-controls')
@speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed()
@progressSlider = new VideoProgressSlider el: @$('.slider')
@playerVars =
controls: 0
wmode: 'transparent'
rel: 0
showinfo: 0
enablejsapi: 1
modestbranding: 1
if @video.start
@playerVars.start = @video.start
@playerVars.wmode = 'window'
if @video.end
# work in AS3, not HMLT5. but iframe use AS3
@playerVars.end = @video.end
@player = new YT.Player @video.id,
playerVars: @playerVars
videoId: @video.youtubeId()
events:
onReady: @onReady
onStateChange: @onStateChange
onPlaybackQualityChange: @onPlaybackQualityChange
@caption.hideCaptions(@['video'].hide_captions)
addToolTip: ->
@$('.add-fullscreen, .hide-subtitles').qtip
position:
my: 'top right'
at: 'top center'
onReady: (event) =>
unless onTouchBasedDevice() or $('.video:first').data('autoplay') == 'False'
$('.video-load-complete:first').data('video').player.play()
onStateChange: (event) =>
switch event.data
when YT.PlayerState.UNSTARTED
@onUnstarted()
when YT.PlayerState.PLAYING
@onPlay()
when YT.PlayerState.PAUSED
@onPause()
when YT.PlayerState.ENDED
@onEnded()
onPlaybackQualityChange: (event, value) =>
quality = @player.getPlaybackQuality()
@qualityControl.onQualityChange(quality)
handlePlaybackQualityChange: (event, value) =>
@player.setPlaybackQuality(value)
onUnstarted: =>
@control.pause()
@caption.pause()
onPlay: =>
@video.log 'play_video'
window.player.pauseVideo() if window.player && window.player != @player
window.player = @player
unless @player.interval
@player.interval = setInterval(@update, 200)
@caption.play()
@control.play()
@progressSlider.play()
onPause: =>
@video.log 'pause_video'
window.player = null if window.player == @player
clearInterval(@player.interval)
@player.interval = null
@caption.pause()
@control.pause()
onEnded: =>
@control.pause()
@caption.pause()
onSeek: (event, time) =>
@player.seekTo(time, true)
if @isPlaying()
clearInterval(@player.interval)
@player.interval = setInterval(@update, 200)
else
@currentTime = time
@updatePlayTime time
onSpeedChange: (event, newSpeed) =>
@currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed)
newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0'
@video.setSpeed(newSpeed)
@caption.currentSpeed = newSpeed
if @isPlaying()
@player.loadVideoById(@video.youtubeId(), @currentTime)
else
@player.cueVideoById(@video.youtubeId(), @currentTime)
@updatePlayTime @currentTime
onVolumeChange: (event, volume) =>
@player.setVolume volume
update: =>
if @currentTime = @player.getCurrentTime()
@updatePlayTime @currentTime
updatePlayTime: (time) ->
progress = Time.format(time) + ' / ' + Time.format(@duration())
@$(".vidtime").html(progress)
@caption.updatePlayTime(time)
@progressSlider.updatePlayTime(time, @duration())
toggleFullScreen: (event) =>
event.preventDefault()
if @el.hasClass('fullscreen')
@$('.add-fullscreen').attr('title', 'Fill browser')
@el.removeClass('fullscreen')
else
@el.addClass('fullscreen')
@$('.add-fullscreen').attr('title', 'Exit fill browser')
@caption.resize()
# Delegates
play: =>
@player.playVideo() if @player.playVideo
isPlaying: ->
@player.getPlayerState() == YT.PlayerState.PLAYING
pause: =>
@player.pauseVideo() if @player.pauseVideo
duration: ->
@video.getDuration()
currentSpeed: ->
@video.speed
volume: (value) ->
if value?
@player.setVolume value
else
@player.getVolume()
class @VideoProgressSlider extends Subview
initialize: ->
@buildSlider() unless onTouchBasedDevice()
buildSlider: ->
@slider = @el.slider
range: 'min'
change: @onChange
slide: @onSlide
stop: @onStop
@buildHandle()
buildHandle: ->
@handle = @$('.ui-slider-handle')
@handle.qtip
content: "#{Time.format(@slider.slider('value'))}"
position:
my: 'bottom center'
at: 'top center'
container: @handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
play: =>
@buildSlider() unless @slider
updatePlayTime: (currentTime, duration) ->
if @slider && !@frozen
@slider.slider('option', 'max', duration)
@slider.slider('value', currentTime)
onSlide: (event, ui) =>
@frozen = true
@updateTooltip(ui.value)
$(@).trigger('seek', ui.value)
onChange: (event, ui) =>
@updateTooltip(ui.value)
onStop: (event, ui) =>
@frozen = true
$(@).trigger('seek', ui.value)
setTimeout (=> @frozen = false), 200
updateTooltip: (value)->
@handle.qtip('option', 'content.text', "#{Time.format(value)}")
class @VideoQualityControl extends Subview
initialize: ->
@quality = null;
bind: ->
@$('.quality_control').click @toggleQuality
render: ->
@el.append """
<a href="#" class="quality_control" title="HD">HD</a>
"""#"
onQualityChange: (value) ->
@quality = value
if @quality in ['hd720', 'hd1080', 'highres']
@el.addClass('active')
else
@el.removeClass('active')
toggleQuality: (event) =>
event.preventDefault()
if @quality in ['hd720', 'hd1080', 'highres']
newQuality = 'large'
else
newQuality = 'hd720'
$(@).trigger('changeQuality', newQuality)
\ No newline at end of file
class @VideoSpeedControl extends Subview
bind: ->
@$('.video_speeds a').click @changeVideoSpeed
if onTouchBasedDevice()
@$('.speeds').click (event) ->
event.preventDefault()
$(this).toggleClass('open')
else
@$('.speeds').mouseenter ->
$(this).addClass('open')
@$('.speeds').mouseleave ->
$(this).removeClass('open')
@$('.speeds').click (event) ->
event.preventDefault()
$(this).removeClass('open')
render: ->
@el.prepend """
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
</a>
<ol class="video_speeds"></ol>
</div>
"""
$.each @speeds, (index, speed) =>
link = $('<a>').attr(href: "#").html("#{speed}x")
@$('.video_speeds').prepend($('<li>').attr('data-speed', speed).html(link))
@setSpeed(@currentSpeed)
changeVideoSpeed: (event) =>
event.preventDefault()
unless $(event.target).parent().hasClass('active')
@currentSpeed = $(event.target).parent().data('speed')
$(@).trigger 'speedChange', $(event.target).parent().data('speed')
@setSpeed(parseFloat(@currentSpeed).toFixed(2).replace /\.00$/, '.0')
setSpeed: (speed) ->
@$('.video_speeds li').removeClass('active')
@$(".video_speeds li[data-speed='#{speed}']").addClass('active')
@$('.speeds p.active').html("#{speed}x")
class @VideoVolumeControl extends Subview
initialize: ->
@currentVolume = 100
bind: ->
@$('.volume').mouseenter ->
$(this).addClass('open')
@$('.volume').mouseleave ->
$(this).removeClass('open')
@$('.volume>a').click(@toggleMute)
render: ->
@el.prepend """
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
"""#"
@slider = @$('.volume-slider').slider
orientation: "vertical"
range: "min"
min: 0
max: 100
value: 100
change: @onChange
slide: @onChange
onChange: (event, ui) =>
@currentVolume = ui.value
$(@).trigger 'volumeChange', @currentVolume
@$('.volume').toggleClass 'muted', @currentVolume == 0
toggleMute: =>
if @currentVolume > 0
@previousVolume = @currentVolume
@slider.slider 'option', 'value', 0
else
@slider.slider 'option', 'value', @previousVolume
...@@ -33,7 +33,7 @@ class TabsEditingDescriptorTestCase(unittest.TestCase): ...@@ -33,7 +33,7 @@ class TabsEditingDescriptorTestCase(unittest.TestCase):
}, },
{ {
'name': "Subtitles", 'name': "Subtitles",
'template': "videoalpha/subtitles.html", 'template': "video/subtitles.html",
}, },
{ {
'name': "Settings", 'name': "Settings",
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
#pylint: disable=W0212 #pylint: disable=W0212
"""Test for Video Alpha Xmodule functional logic. """Test for Video Xmodule functional logic.
These test data read from xml, not from mongo. These test data read from xml, not from mongo.
We have a ModuleStoreTestCase class defined in We have a ModuleStoreTestCase class defined in
...@@ -17,37 +17,36 @@ import unittest ...@@ -17,37 +17,36 @@ import unittest
from . import LogicTest from . import LogicTest
from .import get_test_system from .import get_test_system
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.videoalpha_module import VideoAlphaDescriptor, _create_youtube_string from xmodule.video_module import VideoDescriptor, _create_youtube_string
from xmodule.video_module import VideoDescriptor
from .test_import import DummySystem from .test_import import DummySystem
from textwrap import dedent from textwrap import dedent
class VideoAlphaModuleTest(LogicTest): class VideoModuleTest(LogicTest):
"""Logic tests for VideoAlpha Xmodule.""" """Logic tests for Video Xmodule."""
descriptor_class = VideoAlphaDescriptor descriptor_class = VideoDescriptor
raw_model_data = { raw_model_data = {
'data': '<videoalpha />' 'data': '<video />'
} }
def test_parse_time_empty(self): def test_parse_time_empty(self):
"""Ensure parse_time returns correctly with None or empty string.""" """Ensure parse_time returns correctly with None or empty string."""
expected = '' expected = ''
self.assertEqual(VideoAlphaDescriptor._parse_time(None), expected) self.assertEqual(VideoDescriptor._parse_time(None), expected)
self.assertEqual(VideoAlphaDescriptor._parse_time(''), expected) self.assertEqual(VideoDescriptor._parse_time(''), expected)
def test_parse_time(self): def test_parse_time(self):
"""Ensure that times are parsed correctly into seconds.""" """Ensure that times are parsed correctly into seconds."""
expected = 247 expected = 247
output = VideoAlphaDescriptor._parse_time('00:04:07') output = VideoDescriptor._parse_time('00:04:07')
self.assertEqual(output, expected) self.assertEqual(output, expected)
def test_parse_youtube(self): def test_parse_youtube(self):
"""Test parsing old-style Youtube ID strings into a dict.""" """Test parsing old-style Youtube ID strings into a dict."""
youtube_str = '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg' youtube_str = '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg'
output = VideoAlphaDescriptor._parse_youtube(youtube_str) output = VideoDescriptor._parse_youtube(youtube_str)
self.assertEqual(output, {'0.75': 'jNCf2gIqpeE', self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
'1.00': 'ZwkTiUPN0mg', '1.00': 'ZwkTiUPN0mg',
'1.25': 'rsq9auxASqI', '1.25': 'rsq9auxASqI',
...@@ -59,7 +58,7 @@ class VideoAlphaModuleTest(LogicTest): ...@@ -59,7 +58,7 @@ class VideoAlphaModuleTest(LogicTest):
empty string. empty string.
""" """
youtube_str = '0.75:jNCf2gIqpeE' youtube_str = '0.75:jNCf2gIqpeE'
output = VideoAlphaDescriptor._parse_youtube(youtube_str) output = VideoDescriptor._parse_youtube(youtube_str)
self.assertEqual(output, {'0.75': 'jNCf2gIqpeE', self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
'1.00': '', '1.00': '',
'1.25': '', '1.25': '',
...@@ -72,8 +71,8 @@ class VideoAlphaModuleTest(LogicTest): ...@@ -72,8 +71,8 @@ class VideoAlphaModuleTest(LogicTest):
youtube_str = '1.00:p2Q6BrNhdh8' youtube_str = '1.00:p2Q6BrNhdh8'
youtube_str_hack = '1.0:p2Q6BrNhdh8' youtube_str_hack = '1.0:p2Q6BrNhdh8'
self.assertEqual( self.assertEqual(
VideoAlphaDescriptor._parse_youtube(youtube_str), VideoDescriptor._parse_youtube(youtube_str),
VideoAlphaDescriptor._parse_youtube(youtube_str_hack) VideoDescriptor._parse_youtube(youtube_str_hack)
) )
def test_parse_youtube_empty(self): def test_parse_youtube_empty(self):
...@@ -82,7 +81,7 @@ class VideoAlphaModuleTest(LogicTest): ...@@ -82,7 +81,7 @@ class VideoAlphaModuleTest(LogicTest):
that well. that well.
""" """
self.assertEqual( self.assertEqual(
VideoAlphaDescriptor._parse_youtube(''), VideoDescriptor._parse_youtube(''),
{'0.75': '', {'0.75': '',
'1.00': '', '1.00': '',
'1.25': '', '1.25': '',
...@@ -90,12 +89,12 @@ class VideoAlphaModuleTest(LogicTest): ...@@ -90,12 +89,12 @@ class VideoAlphaModuleTest(LogicTest):
) )
class VideoAlphaDescriptorTest(unittest.TestCase): class VideoDescriptorTest(unittest.TestCase):
"""Test for VideoAlphaDescriptor""" """Test for VideoDescriptor"""
def setUp(self): def setUp(self):
system = get_test_system() system = get_test_system()
self.descriptor = VideoAlphaDescriptor( self.descriptor = VideoDescriptor(
runtime=system, runtime=system,
model_data={}) model_data={})
...@@ -117,9 +116,9 @@ class VideoAlphaDescriptorTest(unittest.TestCase): ...@@ -117,9 +116,9 @@ class VideoAlphaDescriptorTest(unittest.TestCase):
back out to XML. back out to XML.
""" """
system = DummySystem(load_error_modules=True) system = DummySystem(load_error_modules=True)
location = Location(["i4x", "edX", "videoalpha", "default", "SampleProblem1"]) location = Location(["i4x", "edX", "video", "default", "SampleProblem1"])
model_data = {'location': location} model_data = {'location': location}
descriptor = VideoAlphaDescriptor(system, model_data) descriptor = VideoDescriptor(system, model_data)
descriptor.youtube_id_0_75 = 'izygArpw-Qo' descriptor.youtube_id_0_75 = 'izygArpw-Qo'
descriptor.youtube_id_1_0 = 'p2Q6BrNhdh8' descriptor.youtube_id_1_0 = 'p2Q6BrNhdh8'
descriptor.youtube_id_1_25 = '1EeWXzPdhSA' descriptor.youtube_id_1_25 = '1EeWXzPdhSA'
...@@ -133,9 +132,9 @@ class VideoAlphaDescriptorTest(unittest.TestCase): ...@@ -133,9 +132,9 @@ class VideoAlphaDescriptorTest(unittest.TestCase):
in the output string. in the output string.
""" """
system = DummySystem(load_error_modules=True) system = DummySystem(load_error_modules=True)
location = Location(["i4x", "edX", "videoalpha", "default", "SampleProblem1"]) location = Location(["i4x", "edX", "video", "default", "SampleProblem1"])
model_data = {'location': location} model_data = {'location': location}
descriptor = VideoAlphaDescriptor(system, model_data) descriptor = VideoDescriptor(system, model_data)
descriptor.youtube_id_0_75 = 'izygArpw-Qo' descriptor.youtube_id_0_75 = 'izygArpw-Qo'
descriptor.youtube_id_1_0 = 'p2Q6BrNhdh8' descriptor.youtube_id_1_0 = 'p2Q6BrNhdh8'
descriptor.youtube_id_1_25 = '1EeWXzPdhSA' descriptor.youtube_id_1_25 = '1EeWXzPdhSA'
...@@ -143,9 +142,9 @@ class VideoAlphaDescriptorTest(unittest.TestCase): ...@@ -143,9 +142,9 @@ class VideoAlphaDescriptorTest(unittest.TestCase):
self.assertEqual(_create_youtube_string(descriptor), expected) self.assertEqual(_create_youtube_string(descriptor), expected)
class VideoAlphaDescriptorImportTestCase(unittest.TestCase): class VideoDescriptorImportTestCase(unittest.TestCase):
""" """
Make sure that VideoAlphaDescriptor can import an old XML-based video correctly. Make sure that VideoDescriptor can import an old XML-based video correctly.
""" """
def assert_attributes_equal(self, video, attrs): def assert_attributes_equal(self, video, attrs):
...@@ -158,7 +157,7 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase): ...@@ -158,7 +157,7 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase):
def test_constructor(self): def test_constructor(self):
sample_xml = ''' sample_xml = '''
<videoalpha display_name="Test Video" <video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8" youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
show_captions="false" show_captions="false"
start_time="00:00:01" start_time="00:00:01"
...@@ -166,14 +165,14 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase): ...@@ -166,14 +165,14 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase):
<source src="http://www.example.com/source.mp4"/> <source src="http://www.example.com/source.mp4"/>
<source src="http://www.example.com/source.ogg"/> <source src="http://www.example.com/source.ogg"/>
<track src="http://www.example.com/track"/> <track src="http://www.example.com/track"/>
</videoalpha> </video>
''' '''
location = Location(["i4x", "edX", "videoalpha", "default", location = Location(["i4x", "edX", "video", "default",
"SampleProblem1"]) "SampleProblem1"])
model_data = {'data': sample_xml, model_data = {'data': sample_xml,
'location': location} 'location': location}
system = DummySystem(load_error_modules=True) system = DummySystem(load_error_modules=True)
descriptor = VideoAlphaDescriptor(system, model_data) descriptor = VideoDescriptor(system, model_data)
self.assert_attributes_equal(descriptor, { self.assert_attributes_equal(descriptor, {
'youtube_id_0_75': 'izygArpw-Qo', 'youtube_id_0_75': 'izygArpw-Qo',
'youtube_id_1_0': 'p2Q6BrNhdh8', 'youtube_id_1_0': 'p2Q6BrNhdh8',
...@@ -190,16 +189,16 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase): ...@@ -190,16 +189,16 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase):
def test_from_xml(self): def test_from_xml(self):
module_system = DummySystem(load_error_modules=True) module_system = DummySystem(load_error_modules=True)
xml_data = ''' xml_data = '''
<videoalpha display_name="Test Video" <video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8" youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
show_captions="false" show_captions="false"
start_time="00:00:01" start_time="00:00:01"
end_time="00:01:00"> end_time="00:01:00">
<source src="http://www.example.com/source.mp4"/> <source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/> <track src="http://www.example.com/track"/>
</videoalpha> </video>
''' '''
output = VideoAlphaDescriptor.from_xml(xml_data, module_system) output = VideoDescriptor.from_xml(xml_data, module_system)
self.assert_attributes_equal(output, { self.assert_attributes_equal(output, {
'youtube_id_0_75': 'izygArpw-Qo', 'youtube_id_0_75': 'izygArpw-Qo',
'youtube_id_1_0': 'p2Q6BrNhdh8', 'youtube_id_1_0': 'p2Q6BrNhdh8',
...@@ -221,14 +220,14 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase): ...@@ -221,14 +220,14 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase):
""" """
module_system = DummySystem(load_error_modules=True) module_system = DummySystem(load_error_modules=True)
xml_data = ''' xml_data = '''
<videoalpha display_name="Test Video" <video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA" youtube="1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA"
show_captions="true"> show_captions="true">
<source src="http://www.example.com/source.mp4"/> <source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/> <track src="http://www.example.com/track"/>
</videoalpha> </video>
''' '''
output = VideoAlphaDescriptor.from_xml(xml_data, module_system) output = VideoDescriptor.from_xml(xml_data, module_system)
self.assert_attributes_equal(output, { self.assert_attributes_equal(output, {
'youtube_id_0_75': '', 'youtube_id_0_75': '',
'youtube_id_1_0': 'p2Q6BrNhdh8', 'youtube_id_1_0': 'p2Q6BrNhdh8',
...@@ -248,8 +247,8 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase): ...@@ -248,8 +247,8 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase):
Make sure settings are correct if none are explicitly set in XML. Make sure settings are correct if none are explicitly set in XML.
""" """
module_system = DummySystem(load_error_modules=True) module_system = DummySystem(load_error_modules=True)
xml_data = '<videoalpha></videoalpha>' xml_data = '<video></video>'
output = VideoAlphaDescriptor.from_xml(xml_data, module_system) output = VideoDescriptor.from_xml(xml_data, module_system)
self.assert_attributes_equal(output, { self.assert_attributes_equal(output, {
'youtube_id_0_75': '', 'youtube_id_0_75': '',
'youtube_id_1_0': 'OEoXaMPEzfM', 'youtube_id_1_0': 'OEoXaMPEzfM',
...@@ -270,16 +269,16 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase): ...@@ -270,16 +269,16 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase):
""" """
module_system = DummySystem(load_error_modules=True) module_system = DummySystem(load_error_modules=True)
xml_data = """ xml_data = """
<videoalpha display_name="Test Video" <video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8" youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
show_captions="false" show_captions="false"
from="00:00:01" from="00:00:01"
to="00:01:00"> to="00:01:00">
<source src="http://www.example.com/source.mp4"/> <source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/> <track src="http://www.example.com/track"/>
</videoalpha> </video>
""" """
output = VideoAlphaDescriptor.from_xml(xml_data, module_system) output = VideoDescriptor.from_xml(xml_data, module_system)
self.assert_attributes_equal(output, { self.assert_attributes_equal(output, {
'youtube_id_0_75': 'izygArpw-Qo', 'youtube_id_0_75': 'izygArpw-Qo',
'youtube_id_1_0': 'p2Q6BrNhdh8', 'youtube_id_1_0': 'p2Q6BrNhdh8',
...@@ -295,7 +294,7 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase): ...@@ -295,7 +294,7 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase):
def test_old_video_data(self): def test_old_video_data(self):
""" """
Ensure that Video Alpha is able to read VideoModule's model data. Ensure that Video is able to read VideoModule's model data.
""" """
module_system = DummySystem(load_error_modules=True) module_system = DummySystem(load_error_modules=True)
xml_data = """ xml_data = """
...@@ -309,8 +308,7 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase): ...@@ -309,8 +308,7 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase):
</video> </video>
""" """
video = VideoDescriptor.from_xml(xml_data, module_system) video = VideoDescriptor.from_xml(xml_data, module_system)
video_alpha = VideoAlphaDescriptor(module_system, video._model_data) self.assert_attributes_equal(video, {
self.assert_attributes_equal(video_alpha, {
'youtube_id_0_75': 'izygArpw-Qo', 'youtube_id_0_75': 'izygArpw-Qo',
'youtube_id_1_0': 'p2Q6BrNhdh8', 'youtube_id_1_0': 'p2Q6BrNhdh8',
'youtube_id_1_25': '1EeWXzPdhSA', 'youtube_id_1_25': '1EeWXzPdhSA',
...@@ -324,17 +322,17 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase): ...@@ -324,17 +322,17 @@ class VideoAlphaDescriptorImportTestCase(unittest.TestCase):
}) })
class VideoAlphaExportTestCase(unittest.TestCase): class VideoExportTestCase(unittest.TestCase):
""" """
Make sure that VideoAlphaDescriptor can export itself to XML Make sure that VideoDescriptor can export itself to XML
correctly. correctly.
""" """
def test_export_to_xml(self): def test_export_to_xml(self):
"""Test that we write the correct XML on export.""" """Test that we write the correct XML on export."""
module_system = DummySystem(load_error_modules=True) module_system = DummySystem(load_error_modules=True)
location = Location(["i4x", "edX", "videoalpha", "default", "SampleProblem1"]) location = Location(["i4x", "edX", "video", "default", "SampleProblem1"])
desc = VideoAlphaDescriptor(module_system, {'location': location}) desc = VideoDescriptor(module_system, {'location': location})
desc.youtube_id_0_75 = 'izygArpw-Qo' desc.youtube_id_0_75 = 'izygArpw-Qo'
desc.youtube_id_1_0 = 'p2Q6BrNhdh8' desc.youtube_id_1_0 = 'p2Q6BrNhdh8'
...@@ -348,11 +346,11 @@ class VideoAlphaExportTestCase(unittest.TestCase): ...@@ -348,11 +346,11 @@ class VideoAlphaExportTestCase(unittest.TestCase):
xml = desc.export_to_xml(None) # We don't use the `resource_fs` parameter xml = desc.export_to_xml(None) # We don't use the `resource_fs` parameter
expected = dedent('''\ expected = dedent('''\
<videoalpha display_name="Video Alpha" start_time="0:00:01" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false" end_time="0:01:00"> <video url_name="SampleProblem1" start_time="0:00:01" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false" end_time="0:01:00">
<source src="http://www.example.com/source.mp4"/> <source src="http://www.example.com/source.mp4"/>
<source src="http://www.example.com/source.ogg"/> <source src="http://www.example.com/source.ogg"/>
<track src="http://www.example.com/track"/> <track src="http://www.example.com/track"/>
</videoalpha> </video>
''') ''')
self.assertEquals(expected, xml) self.assertEquals(expected, xml)
...@@ -360,10 +358,10 @@ class VideoAlphaExportTestCase(unittest.TestCase): ...@@ -360,10 +358,10 @@ class VideoAlphaExportTestCase(unittest.TestCase):
def test_export_to_xml_empty_parameters(self): def test_export_to_xml_empty_parameters(self):
"""Test XML export with defaults.""" """Test XML export with defaults."""
module_system = DummySystem(load_error_modules=True) module_system = DummySystem(load_error_modules=True)
location = Location(["i4x", "edX", "videoalpha", "default", "SampleProblem1"]) location = Location(["i4x", "edX", "video", "default", "SampleProblem1"])
desc = VideoAlphaDescriptor(module_system, {'location': location}) desc = VideoDescriptor(module_system, {'location': location})
xml = desc.export_to_xml(None) xml = desc.export_to_xml(None)
expected = '<videoalpha display_name="Video Alpha" youtube="1.00:OEoXaMPEzfM" show_captions="true"/>\n' expected = '<video url_name="SampleProblem1"/>\n'
self.assertEquals(expected, xml) self.assertEquals(expected, xml)
# -*- coding: utf-8 -*-
import unittest
from xmodule.modulestore import Location
from xmodule.video_module import VideoDescriptor
from .test_import import DummySystem
class VideoDescriptorImportTestCase(unittest.TestCase):
"""
Make sure that VideoDescriptor can import an old XML-based video correctly.
"""
def test_constructor(self):
sample_xml = '''
<video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
show_captions="false"
from="00:00:01"
to="00:01:00">
<source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/>
</video>
'''
location = Location(["i4x", "edX", "video", "default",
"SampleProblem1"])
model_data = {'data': sample_xml,
'location': location}
system = DummySystem(load_error_modules=True)
descriptor = VideoDescriptor(system, model_data)
self.assertEquals(descriptor.youtube_id_0_75, 'izygArpw-Qo')
self.assertEquals(descriptor.youtube_id_1_0, 'p2Q6BrNhdh8')
self.assertEquals(descriptor.youtube_id_1_25, '1EeWXzPdhSA')
self.assertEquals(descriptor.youtube_id_1_5, 'rABDYkeK0x8')
self.assertEquals(descriptor.show_captions, False)
self.assertEquals(descriptor.start_time, 1.0)
self.assertEquals(descriptor.end_time, 60)
self.assertEquals(descriptor.track, 'http://www.example.com/track')
self.assertEquals(descriptor.source, 'http://www.example.com/source.mp4')
def test_from_xml(self):
module_system = DummySystem(load_error_modules=True)
xml_data = '''
<video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
show_captions="false"
from="00:00:01"
to="00:01:00">
<source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/>
</video>
'''
output = VideoDescriptor.from_xml(xml_data, module_system)
self.assertEquals(output.youtube_id_0_75, 'izygArpw-Qo')
self.assertEquals(output.youtube_id_1_0, 'p2Q6BrNhdh8')
self.assertEquals(output.youtube_id_1_25, '1EeWXzPdhSA')
self.assertEquals(output.youtube_id_1_5, 'rABDYkeK0x8')
self.assertEquals(output.show_captions, False)
self.assertEquals(output.start_time, 1.0)
self.assertEquals(output.end_time, 60)
self.assertEquals(output.track, 'http://www.example.com/track')
self.assertEquals(output.source, 'http://www.example.com/source.mp4')
def test_from_xml_missing_attributes(self):
"""
Ensure that attributes have the right values if they aren't
explicitly set in XML.
"""
module_system = DummySystem(load_error_modules=True)
xml_data = '''
<video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA"
show_captions="true">
<source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/>
</video>
'''
output = VideoDescriptor.from_xml(xml_data, module_system)
self.assertEquals(output.youtube_id_0_75, '')
self.assertEquals(output.youtube_id_1_0, 'p2Q6BrNhdh8')
self.assertEquals(output.youtube_id_1_25, '1EeWXzPdhSA')
self.assertEquals(output.youtube_id_1_5, '')
self.assertEquals(output.show_captions, True)
self.assertEquals(output.start_time, 0.0)
self.assertEquals(output.end_time, 0.0)
self.assertEquals(output.track, 'http://www.example.com/track')
self.assertEquals(output.source, 'http://www.example.com/source.mp4')
def test_from_xml_no_attributes(self):
"""
Make sure settings are correct if none are explicitly set in XML.
"""
module_system = DummySystem(load_error_modules=True)
xml_data = '<video></video>'
output = VideoDescriptor.from_xml(xml_data, module_system)
self.assertEquals(output.youtube_id_0_75, '')
self.assertEquals(output.youtube_id_1_0, 'OEoXaMPEzfM')
self.assertEquals(output.youtube_id_1_25, '')
self.assertEquals(output.youtube_id_1_5, '')
self.assertEquals(output.show_captions, True)
self.assertEquals(output.start_time, 0.0)
self.assertEquals(output.end_time, 0.0)
self.assertEquals(output.track, '')
self.assertEquals(output.source, '')
# -*- coding: utf-8 -*-
"""Test for Video Xmodule functional logic.
These tests data readed from xml, not from mongo.
We have a ModuleStoreTestCase class defined in
common/lib/xmodule/xmodule/modulestore/tests/django_utils.py.
You can search for usages of this in the cms and lms tests for examples.
You use this so that it will do things like point the modulestore
setting to mongo, flush the contentstore before and after, load the
templates, etc.
You can then use the CourseFactory and XModuleItemFactory as defined in
common/lib/xmodule/xmodule/modulestore/tests/factories.py to create the
course, section, subsection, unit, etc.
"""
from mock import Mock
from xmodule.video_module import VideoDescriptor, VideoModule, _parse_time, _parse_youtube
from xmodule.modulestore import Location
from xmodule.tests import get_test_system
from xmodule.tests import LogicTest
class VideoFactory(object):
"""A helper class to create video modules with various parameters
for testing.
"""
# tag that uses youtube videos
sample_problem_xml_youtube = """
<video show_captions="true"
youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg"
data_dir=""
caption_asset_path=""
autoplay="true"
from="01:00:03" to="01:00:10"
>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.mp4"/>
</video>
"""
@staticmethod
def create():
"""Method return Video Xmodule instance."""
location = Location(["i4x", "edX", "video", "default",
"SampleProblem1"])
model_data = {'data': VideoFactory.sample_problem_xml_youtube, 'location': location}
descriptor = Mock(weight="1", url_name="SampleProblem1")
system = get_test_system()
system.render_template = lambda template, context: context
module = VideoModule(system, descriptor, model_data)
return module
class VideoModuleLogicTest(LogicTest):
"""Tests for logic of Video Xmodule."""
descriptor_class = VideoDescriptor
raw_model_data = {
'data': '<video />'
}
def test_parse_time(self):
"""Ensure that times are parsed correctly into seconds."""
output = _parse_time('00:04:07')
self.assertEqual(output, 247)
def test_parse_time_none(self):
"""Check parsing of None."""
output = _parse_time(None)
self.assertEqual(output, '')
def test_parse_time_empty(self):
"""Check parsing of the empty string."""
output = _parse_time('')
self.assertEqual(output, '')
def test_parse_youtube(self):
"""Test parsing old-style Youtube ID strings into a dict."""
youtube_str = '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg'
output = _parse_youtube(youtube_str)
self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
'1.00': 'ZwkTiUPN0mg',
'1.25': 'rsq9auxASqI',
'1.50': 'kMyNdzVHHgg'})
def test_parse_youtube_one_video(self):
"""
Ensure that all keys are present and missing speeds map to the
empty string.
"""
youtube_str = '0.75:jNCf2gIqpeE'
output = _parse_youtube(youtube_str)
self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
'1.00': '',
'1.25': '',
'1.50': ''})
def test_parse_youtube_key_format(self):
"""
Make sure that inconsistent speed keys are parsed correctly.
"""
youtube_str = '1.00:p2Q6BrNhdh8'
youtube_str_hack = '1.0:p2Q6BrNhdh8'
self.assertEqual(_parse_youtube(youtube_str), _parse_youtube(youtube_str_hack))
def test_parse_youtube_empty(self):
"""
Some courses have empty youtube attributes, so we should handle
that well.
"""
self.assertEqual(_parse_youtube(''),
{'0.75': '',
'1.00': '',
'1.25': '',
'1.50': ''})
...@@ -16,10 +16,9 @@ from xmodule.gst_module import GraphicalSliderToolDescriptor ...@@ -16,10 +16,9 @@ from xmodule.gst_module import GraphicalSliderToolDescriptor
from xmodule.html_module import HtmlDescriptor from xmodule.html_module import HtmlDescriptor
from xmodule.peer_grading_module import PeerGradingDescriptor from xmodule.peer_grading_module import PeerGradingDescriptor
from xmodule.poll_module import PollDescriptor from xmodule.poll_module import PollDescriptor
from xmodule.video_module import VideoDescriptor
from xmodule.word_cloud_module import WordCloudDescriptor from xmodule.word_cloud_module import WordCloudDescriptor
from xmodule.crowdsource_hinter import CrowdsourceHinterDescriptor from xmodule.crowdsource_hinter import CrowdsourceHinterDescriptor
from xmodule.videoalpha_module import VideoAlphaDescriptor from xmodule.video_module import VideoDescriptor
from xmodule.seq_module import SequenceDescriptor from xmodule.seq_module import SequenceDescriptor
from xmodule.conditional_module import ConditionalDescriptor from xmodule.conditional_module import ConditionalDescriptor
from xmodule.randomize_module import RandomizeDescriptor from xmodule.randomize_module import RandomizeDescriptor
...@@ -35,9 +34,8 @@ LEAF_XMODULES = ( ...@@ -35,9 +34,8 @@ LEAF_XMODULES = (
HtmlDescriptor, HtmlDescriptor,
PeerGradingDescriptor, PeerGradingDescriptor,
PollDescriptor, PollDescriptor,
VideoDescriptor,
# This is being excluded because it has dependencies on django # This is being excluded because it has dependencies on django
#VideoAlphaDescriptor, #VideoDescriptor,
WordCloudDescriptor, WordCloudDescriptor,
) )
......
# pylint: disable=W0223 # pylint: disable=W0223
"""Video is ungraded Xmodule for support video content.""" """Video is ungraded Xmodule for support video content.
It's new improved video module, which support additional feature:
- Can play non-YouTube video sources via in-browser HTML5 video player.
- YouTube defaults to HTML5 mode from the start.
- Speed changes in both YouTube and non-YouTube videos happen via
in-browser HTML5 video method (when in HTML5 mode).
- Navigational subtitles can be disabled altogether via an attribute
in XML.
"""
import json import json
import logging import logging
from lxml import etree from lxml import etree
from pkg_resources import resource_string, resource_listdir from pkg_resources import resource_string
import datetime
import time
from django.http import Http404 from django.http import Http404
from django.conf import settings
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.editing_module import TabsEditingDescriptor
from xmodule.raw_module import EmptyDataRawDescriptor from xmodule.raw_module import EmptyDataRawDescriptor
from xmodule.editing_module import MetadataOnlyEditingDescriptor from xmodule.xml_module import is_pointer_tag, name_to_pathname
from xblock.core import Integer, Scope, String, Float, Boolean from xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
from xblock.core import Scope, String, Boolean, Float, List, Integer
import datetime
import time
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -22,51 +38,118 @@ log = logging.getLogger(__name__) ...@@ -22,51 +38,118 @@ log = logging.getLogger(__name__)
class VideoFields(object): class VideoFields(object):
"""Fields for `VideoModule` and `VideoDescriptor`.""" """Fields for `VideoModule` and `VideoDescriptor`."""
display_name = String( display_name = String(
display_name="Display Name", display_name="Display Name", help="Display name for this module.",
help="This name appears in the horizontal navigation at the top of the page.", default="Video",
scope=Scope.settings
)
position = Integer(
help="Current position in the video",
scope=Scope.user_state,
default=0
)
show_captions = Boolean(
help="This controls whether or not captions are shown by default.",
display_name="Show Captions",
scope=Scope.settings, scope=Scope.settings,
# it'd be nice to have a useful default but it screws up other things; so, default=True
# use display_name_with_default for those
default="Video"
) )
data = String( # TODO: This should be moved to Scope.content, but this will
help="XML data for the problem", # require data migration to support the old video module.
default='', youtube_id_1_0 = String(
scope=Scope.content help="This is the Youtube ID reference for the normal speed video.",
display_name="Youtube ID",
scope=Scope.settings,
default="OEoXaMPEzfM"
)
youtube_id_0_75 = String(
help="The Youtube ID for the .75x speed video.",
display_name="Youtube ID for .75x speed",
scope=Scope.settings,
default=""
)
youtube_id_1_25 = String(
help="The Youtube ID for the 1.25x speed video.",
display_name="Youtube ID for 1.25x speed",
scope=Scope.settings,
default=""
)
youtube_id_1_5 = String(
help="The Youtube ID for the 1.5x speed video.",
display_name="Youtube ID for 1.5x speed",
scope=Scope.settings,
default=""
)
start_time = Float(
help="Start time for the video.",
display_name="Start Time",
scope=Scope.settings,
default=0.0
)
end_time = Float(
help="End time for the video.",
display_name="End Time",
scope=Scope.settings,
default=0.0
)
source = String(
help="The external URL to download the video. This appears as a link beneath the video.",
display_name="Download Video",
scope=Scope.settings,
default=""
)
html5_sources = List(
help="A list of filenames to be used with HTML5 video. The first supported filetype will be displayed.",
display_name="Video Sources",
scope=Scope.settings,
default=[]
)
track = String(
help="The external URL to download the subtitle track. This appears as a link beneath the video.",
display_name="Download Track",
scope=Scope.settings,
default=""
)
sub = String(
help="The name of the subtitle track (for non-Youtube videos).",
display_name="HTML5 Subtitles",
scope=Scope.settings,
default=""
) )
position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
show_captions = Boolean(help="This controls whether or not captions are shown by default.", display_name="Show Captions", scope=Scope.settings, default=True)
youtube_id_1_0 = String(help="This is the Youtube ID reference for the normal speed video.", display_name="Default Speed", scope=Scope.settings, default="OEoXaMPEzfM")
youtube_id_0_75 = String(help="The Youtube ID for the .75x speed video.", display_name="Speed: .75x", scope=Scope.settings, default="")
youtube_id_1_25 = String(help="The Youtube ID for the 1.25x speed video.", display_name="Speed: 1.25x", scope=Scope.settings, default="")
youtube_id_1_5 = String(help="The Youtube ID for the 1.5x speed video.", display_name="Speed: 1.5x", scope=Scope.settings, default="")
start_time = Float(help="Time the video starts", display_name="Start Time", scope=Scope.settings, default=0.0)
end_time = Float(help="Time the video ends", display_name="End Time", scope=Scope.settings, default=0.0)
source = String(help="The external URL to download the video. This appears as a link beneath the video.", display_name="Download Video", scope=Scope.settings, default="")
track = String(help="The external URL to download the subtitle track. This appears as a link beneath the video.", display_name="Download Track", scope=Scope.settings, default="")
class VideoModule(VideoFields, XModule): class VideoModule(VideoFields, XModule):
"""Video Xmodule.""" """
XML source example:
<video show_captions="true"
youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg"
url_name="lecture_21_3" display_name="S19V3: Vacancies"
>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.mp4"/>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.webm"/>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.ogv"/>
</video>
"""
video_time = 0 video_time = 0
icon_class = 'video' icon_class = 'video'
js = { js = {
'coffee': [ 'js': [
resource_string(__name__, 'js/src/time.coffee'), resource_string(__name__, 'js/src/video/01_initialize.js'),
resource_string(__name__, 'js/src/video/display.coffee') 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/display/' + filename) resource_string(__name__, 'js/src/video/04_video_control.js'),
for filename resource_string(__name__, 'js/src/video/05_video_quality_control.js'),
in sorted(resource_listdir(__name__, 'js/src/video/display')) resource_string(__name__, 'js/src/video/06_video_progress_slider.js'),
if filename.endswith('.coffee')] 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')
]
} }
css = {'scss': [resource_string(__name__, 'css/video/display.scss')]} css = {'scss': [resource_string(__name__, 'css/video/display.scss')]}
js_module_name = "Video" js_module_name = "Video"
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
def handle_ajax(self, dispatch, data): def handle_ajax(self, dispatch, data):
"""This is not being called right now and we raise 404 error.""" """This is not being called right now and we raise 404 error."""
log.debug(u"GET {0}".format(data)) log.debug(u"GET {0}".format(data))
...@@ -78,41 +161,59 @@ class VideoModule(VideoFields, XModule): ...@@ -78,41 +161,59 @@ class VideoModule(VideoFields, XModule):
return json.dumps({'position': self.position}) return json.dumps({'position': self.position})
def get_html(self): def get_html(self):
if isinstance(modulestore(), MongoModuleStore):
caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_'
else:
# VS[compat]
# cdodge: filesystem static content support.
caption_asset_path = "/static/subs/"
get_ext = lambda filename: filename.rpartition('.')[-1]
sources = {get_ext(src): src for src in self.html5_sources}
sources['main'] = self.source
return self.system.render_template('video.html', { return self.system.render_template('video.html', {
'youtube_id_0_75': self.youtube_id_0_75, 'youtube_streams': _create_youtube_string(self),
'youtube_id_1_0': self.youtube_id_1_0,
'youtube_id_1_25': self.youtube_id_1_25,
'youtube_id_1_5': self.youtube_id_1_5,
'id': self.location.html_id(), 'id': self.location.html_id(),
'position': self.position, 'sub': self.sub,
'source': self.source, 'sources': sources,
'track': self.track, 'track': self.track,
'display_name': self.display_name_with_default, 'display_name': self.display_name_with_default,
'caption_asset_path': "/static/subs/", # This won't work when we move to data that
'show_captions': 'true' if self.show_captions else 'false', # isn't on the filesystem
'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': caption_asset_path,
'show_captions': json.dumps(self.show_captions),
'start': self.start_time, 'start': self.start_time,
'end': self.end_time 'end': self.end_time,
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
}) })
class VideoDescriptor(VideoFields, class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor):
MetadataOnlyEditingDescriptor, """Descriptor for `VideoModule`."""
EmptyDataRawDescriptor):
module_class = VideoModule module_class = VideoModule
tabs = [
# {
# 'name': "Subtitles",
# 'template': "video/subtitles.html",
# },
{
'name': "Settings",
'template': "tabs/metadata-edit-tab.html",
'current': True
}
]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(VideoDescriptor, self).__init__(*args, **kwargs) super(VideoDescriptor, self).__init__(*args, **kwargs)
# If we don't have a `youtube_id_1_0`, this is an XML course # For backwards compatibility -- if we've got XML data, parse
# and we parse out the fields. # it out and set the metadata fields
if self.data and 'youtube_id_1_0' not in self._model_data: if self.data:
_parse_video_xml(self, self.data) model_data = VideoDescriptor._parse_video_xml(self.data)
self._model_data.update(model_data)
@property del self.data
def non_editable_metadata_fields(self):
non_editable_fields = super(MetadataOnlyEditingDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([VideoModule.start_time,
VideoModule.end_time])
return non_editable_fields
@classmethod @classmethod
def from_xml(cls, xml_data, system, org=None, course=None): def from_xml(cls, xml_data, system, org=None, course=None):
...@@ -126,102 +227,164 @@ class VideoDescriptor(VideoFields, ...@@ -126,102 +227,164 @@ class VideoDescriptor(VideoFields,
org and course are optional strings that will be used in the generated modules org and course are optional strings that will be used in the generated modules
url identifiers url identifiers
""" """
video = super(VideoDescriptor, cls).from_xml(xml_data, system, org, course) xml_object = etree.fromstring(xml_data)
_parse_video_xml(video, video.data) url_name = xml_object.get('url_name', xml_object.get('slug'))
location = Location(
'i4x', org, course, 'video', url_name
)
if is_pointer_tag(xml_object):
filepath = cls._format_filepath(xml_object.tag, name_to_pathname(url_name))
xml_data = etree.tostring(cls.load_file(filepath, system.resources_fs, location))
model_data = VideoDescriptor._parse_video_xml(xml_data)
model_data['location'] = location
video = cls(system, model_data)
return video return video
def export_to_xml(self, resource_fs):
"""
Returns an xml string representing this module.
"""
xml = etree.Element('video')
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 == '1.00:OEoXaMPEzfM':
youtube_string = ''
attrs = {
'display_name': self.display_name,
'show_captions': json.dumps(self.show_captions),
'youtube': youtube_string,
'start_time': datetime.timedelta(seconds=self.start_time),
'end_time': datetime.timedelta(seconds=self.end_time),
'sub': self.sub,
'url_name': self.url_name
}
fields = {field.name: field for field in self.fields}
for key, value in attrs.items():
# 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 key in fields and fields[key].default == getattr(self, key):
continue
if value:
xml.set(key, str(value))
for source in self.html5_sources:
ele = etree.Element('source')
ele.set('src', source)
xml.append(ele)
if self.track:
ele = etree.Element('track')
ele.set('src', self.track)
xml.append(ele)
return etree.tostring(xml, pretty_print=True)
@staticmethod
def _parse_youtube(data):
"""
Parses a string of Youtube IDs such as "1.0:AXdE34_U,1.5:VO3SxfeD"
into a dictionary. Necessary for backwards compatibility with
XML-based courses.
"""
ret = {'0.75': '', '1.00': '', '1.25': '', '1.50': ''}
if data == '':
return ret
videos = data.split(',')
for video in videos:
pieces = video.split(':')
# HACK
# To elaborate somewhat: in many LMS tests, the keys for
# Youtube IDs are inconsistent. Sometimes a particular
# speed isn't present, and formatting is also inconsistent
# ('1.0' versus '1.00'). So it's necessary to either do
# something like this or update all the tests to work
# properly.
ret['%.2f' % float(pieces[0])] = pieces[1]
return ret
def _parse_video_xml(video, xml_data): @staticmethod
""" def _parse_video_xml(xml_data):
Parse video fields out of xml_data. The fields are set if they are """
present in the XML. Parse video fields out of xml_data. The fields are set if they are
""" present in the XML.
if not xml_data: """
return xml = etree.fromstring(xml_data)
model_data = {}
xml = etree.fromstring(xml_data)
conversions = {
display_name = xml.get('display_name') 'show_captions': json.loads,
if display_name: 'start_time': VideoDescriptor._parse_time,
video.display_name = display_name 'end_time': VideoDescriptor._parse_time
}
youtube = xml.get('youtube')
if youtube: # Convert between key names for certain attributes --
speeds = _parse_youtube(youtube) # necessary for backwards compatibility.
if speeds['0.75']: compat_keys = {
video.youtube_id_0_75 = speeds['0.75'] 'from': 'start_time',
if speeds['1.00']: 'to': 'end_time'
video.youtube_id_1_0 = speeds['1.00'] }
if speeds['1.25']:
video.youtube_id_1_25 = speeds['1.25'] sources = xml.findall('source')
if speeds['1.50']: if sources:
video.youtube_id_1_5 = speeds['1.50'] model_data['html5_sources'] = [ele.get('src') for ele in sources]
model_data['source'] = model_data['html5_sources'][0]
show_captions = xml.get('show_captions')
if show_captions: track = xml.find('track')
video.show_captions = json.loads(show_captions) if track is not None:
model_data['track'] = track.get('src')
source = _get_first_external(xml, 'source')
if source: for attr, value in xml.items():
video.source = source if attr in compat_keys:
attr = compat_keys[attr]
track = _get_first_external(xml, 'track') if attr in VideoDescriptor.metadata_to_strip + ('url_name', 'name'):
if track: continue
video.track = track if attr == 'youtube':
speeds = VideoDescriptor._parse_youtube(value)
start_time = _parse_time(xml.get('from')) for speed, youtube_id in speeds.items():
if start_time: # should have made these youtube_id_1_00 for
video.start_time = start_time # cleanliness, but hindsight doesn't need glasses
normalized_speed = speed[:-1] if speed.endswith('0') else speed
end_time = _parse_time(xml.get('to')) # If the user has specified html5 sources, make sure we don't use the default video
if end_time: if youtube_id != '' or 'html5_sources' in model_data:
video.end_time = end_time model_data['youtube_id_{0}'.format(normalized_speed.replace('.', '_'))] = youtube_id
else:
# Convert XML attrs into Python values.
def _get_first_external(xmltree, tag): if attr in conversions:
""" value = conversions[attr](value)
Returns the src attribute of the nested `tag` in `xmltree`, if it model_data[attr] = value
exists.
""" return model_data
for element in xmltree.findall(tag):
src = element.get('src') @staticmethod
if src: def _parse_time(str_time):
return src """Converts s in '12:34:45' format to seconds. If s is
return None None, returns empty string"""
if not str_time:
return ''
def _parse_youtube(data): else:
obj_time = time.strptime(str_time, '%H:%M:%S')
return datetime.timedelta(
hours=obj_time.tm_hour,
minutes=obj_time.tm_min,
seconds=obj_time.tm_sec
).total_seconds()
def _create_youtube_string(module):
""" """
Parses a string of Youtube IDs such as "1.0:AXdE34_U,1.5:VO3SxfeD" Create a string of Youtube IDs from `module`'s metadata
into a dictionary. Necessary for backwards compatibility with attributes. Only writes a speed if an ID is present in the
XML-based courses. module. Necessary for backwards compatibility with XML-based
courses.
""" """
ret = {'0.75': '', '1.00': '', '1.25': '', '1.50': ''} youtube_ids = [
if data == '': module.youtube_id_0_75,
return ret module.youtube_id_1_0,
videos = data.split(',') module.youtube_id_1_25,
for video in videos: module.youtube_id_1_5
pieces = video.split(':') ]
# HACK youtube_speeds = ['0.75', '1.00', '1.25', '1.50']
# To elaborate somewhat: in many LMS tests, the keys for return ','.join([':'.join(pair)
# Youtube IDs are inconsistent. Sometimes a particular for pair
# speed isn't present, and formatting is also inconsistent in zip(youtube_speeds, youtube_ids)
# ('1.0' versus '1.00'). So it's necessary to either do if pair[1]])
# something like this or update all the tests to work
# properly.
ret['%.2f' % float(pieces[0])] = pieces[1]
return ret
def _parse_time(str_time):
"""Converts s in '12:34:45' format to seconds. If s is
None, returns empty string"""
if str_time is None or str_time == '':
return ''
else:
obj_time = time.strptime(str_time, '%H:%M:%S')
return datetime.timedelta(
hours=obj_time.tm_hour,
minutes=obj_time.tm_min,
seconds=obj_time.tm_sec
).total_seconds()
# pylint: disable=W0223
"""VideoAlpha is ungraded Xmodule for support video content.
It's new improved video module, which support additional feature:
- Can play non-YouTube video sources via in-browser HTML5 video player.
- YouTube defaults to HTML5 mode from the start.
- Speed changes in both YouTube and non-YouTube videos happen via
in-browser HTML5 video method (when in HTML5 mode).
- Navigational subtitles can be disabled altogether via an attribute
in XML.
"""
import json
import logging
from lxml import etree
from pkg_resources import resource_string
from django.http import Http404
from django.conf import settings
from xmodule.x_module import XModule
from xmodule.editing_module import TabsEditingDescriptor
from xmodule.raw_module import EmptyDataRawDescriptor
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
from xblock.core import Scope, String, Boolean, Float, List, Integer
import datetime
import time
log = logging.getLogger(__name__)
class VideoAlphaFields(object):
"""Fields for `VideoAlphaModule` and `VideoAlphaDescriptor`."""
display_name = String(
display_name="Display Name", help="Display name for this module.",
default="Video Alpha",
scope=Scope.settings
)
position = Integer(
help="Current position in the video",
scope=Scope.user_state,
default=0
)
show_captions = Boolean(
help="This controls whether or not captions are shown by default.",
display_name="Show Captions",
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(
help="This is the Youtube ID reference for the normal speed video.",
display_name="Youtube ID",
scope=Scope.settings,
default="OEoXaMPEzfM"
)
youtube_id_0_75 = String(
help="The Youtube ID for the .75x speed video.",
display_name="Youtube ID for .75x speed",
scope=Scope.settings,
default=""
)
youtube_id_1_25 = String(
help="The Youtube ID for the 1.25x speed video.",
display_name="Youtube ID for 1.25x speed",
scope=Scope.settings,
default=""
)
youtube_id_1_5 = String(
help="The Youtube ID for the 1.5x speed video.",
display_name="Youtube ID for 1.5x speed",
scope=Scope.settings,
default=""
)
start_time = Float(
help="Start time for the video.",
display_name="Start Time",
scope=Scope.settings,
default=0.0
)
end_time = Float(
help="End time for the video.",
display_name="End Time",
scope=Scope.settings,
default=0.0
)
source = String(
help="The external URL to download the video. This appears as a link beneath the video.",
display_name="Download Video",
scope=Scope.settings,
default=""
)
html5_sources = List(
help="A list of filenames to be used with HTML5 video. The first supported filetype will be displayed.",
display_name="Video Sources",
scope=Scope.settings,
default=[]
)
track = String(
help="The external URL to download the subtitle track. This appears as a link beneath the video.",
display_name="Download Track",
scope=Scope.settings,
default=""
)
sub = String(
help="The name of the subtitle track (for non-Youtube videos).",
display_name="HTML5 Subtitles",
scope=Scope.settings,
default=""
)
class VideoAlphaModule(VideoAlphaFields, XModule):
"""
XML source example:
<videoalpha show_captions="true"
youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg"
url_name="lecture_21_3" display_name="S19V3: Vacancies"
>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.mp4"/>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.webm"/>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.ogv"/>
</videoalpha>
"""
video_time = 0
icon_class = 'video'
js = {
'js': [
resource_string(__name__, 'js/src/videoalpha/01_initialize.js'),
resource_string(__name__, 'js/src/videoalpha/02_html5_video.js'),
resource_string(__name__, 'js/src/videoalpha/03_video_player.js'),
resource_string(__name__, 'js/src/videoalpha/04_video_control.js'),
resource_string(__name__, 'js/src/videoalpha/05_video_quality_control.js'),
resource_string(__name__, 'js/src/videoalpha/06_video_progress_slider.js'),
resource_string(__name__, 'js/src/videoalpha/07_video_volume_control.js'),
resource_string(__name__, 'js/src/videoalpha/08_video_speed_control.js'),
resource_string(__name__, 'js/src/videoalpha/09_video_caption.js'),
resource_string(__name__, 'js/src/videoalpha/10_main.js')
]
}
css = {'scss': [resource_string(__name__, 'css/videoalpha/display.scss')]}
js_module_name = "VideoAlpha"
def handle_ajax(self, dispatch, data):
"""This is not being called right now and we raise 404 error."""
log.debug(u"GET {0}".format(data))
log.debug(u"DISPATCH {0}".format(dispatch))
raise Http404()
def get_instance_state(self):
"""Return information about state (position)."""
return json.dumps({'position': self.position})
def get_html(self):
if isinstance(modulestore(), MongoModuleStore):
caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_'
else:
# VS[compat]
# cdodge: filesystem static content support.
caption_asset_path = "/static/subs/"
get_ext = lambda filename: filename.rpartition('.')[-1]
sources = {get_ext(src): src for src in self.html5_sources}
sources['main'] = self.source
return self.system.render_template('videoalpha.html', {
'youtube_streams': _create_youtube_string(self),
'id': self.location.html_id(),
'sub': self.sub,
'sources': sources,
'track': self.track,
'display_name': self.display_name_with_default,
# This won't work when we move to data that
# isn't on the filesystem
'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': caption_asset_path,
'show_captions': json.dumps(self.show_captions),
'start': self.start_time,
'end': self.end_time,
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
})
class VideoAlphaDescriptor(VideoAlphaFields, TabsEditingDescriptor, EmptyDataRawDescriptor):
"""Descriptor for `VideoAlphaModule`."""
module_class = VideoAlphaModule
tabs = [
# {
# 'name': "Subtitles",
# 'template': "videoalpha/subtitles.html",
# },
{
'name': "Settings",
'template': "tabs/metadata-edit-tab.html",
'current': True
}
]
def __init__(self, *args, **kwargs):
super(VideoAlphaDescriptor, self).__init__(*args, **kwargs)
# For backwards compatibility -- if we've got XML data, parse
# it out and set the metadata fields
if self.data:
model_data = VideoAlphaDescriptor._parse_video_xml(self.data)
self._model_data.update(model_data)
del self.data
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
"""
Creates an instance of this descriptor from the supplied xml_data.
This may be overridden by subclasses
xml_data: A string of xml that will be translated into data and children for
this module
system: A DescriptorSystem for interacting with external resources
org and course are optional strings that will be used in the generated modules
url identifiers
"""
# Calling from_xml of XmlDescritor, to get right Location, when importing from XML
video = super(VideoAlphaDescriptor, cls).from_xml(xml_data, system, org, course)
return video
def export_to_xml(self, resource_fs):
"""
Returns an xml string representing this module.
"""
xml = etree.Element('videoalpha')
attrs = {
'display_name': self.display_name,
'show_captions': json.dumps(self.show_captions),
'youtube': _create_youtube_string(self),
'start_time': datetime.timedelta(seconds=self.start_time),
'end_time': datetime.timedelta(seconds=self.end_time),
'sub': self.sub
}
for key, value in attrs.items():
if value:
xml.set(key, str(value))
for source in self.html5_sources:
ele = etree.Element('source')
ele.set('src', source)
xml.append(ele)
if self.track:
ele = etree.Element('track')
ele.set('src', self.track)
xml.append(ele)
return etree.tostring(xml, pretty_print=True)
@staticmethod
def _parse_youtube(data):
"""
Parses a string of Youtube IDs such as "1.0:AXdE34_U,1.5:VO3SxfeD"
into a dictionary. Necessary for backwards compatibility with
XML-based courses.
"""
ret = {'0.75': '', '1.00': '', '1.25': '', '1.50': ''}
if data == '':
return ret
videos = data.split(',')
for video in videos:
pieces = video.split(':')
# HACK
# To elaborate somewhat: in many LMS tests, the keys for
# Youtube IDs are inconsistent. Sometimes a particular
# speed isn't present, and formatting is also inconsistent
# ('1.0' versus '1.00'). So it's necessary to either do
# something like this or update all the tests to work
# properly.
ret['%.2f' % float(pieces[0])] = pieces[1]
return ret
@staticmethod
def _parse_video_xml(xml_data):
"""
Parse video fields out of xml_data. The fields are set if they are
present in the XML.
"""
xml = etree.fromstring(xml_data)
model_data = {}
conversions = {
'show_captions': json.loads,
'start_time': VideoAlphaDescriptor._parse_time,
'end_time': VideoAlphaDescriptor._parse_time
}
# VideoModule and VideoAlphaModule use different names for
# these attributes -- need to convert between them
video_compat = {
'from': 'start_time',
'to': 'end_time'
}
sources = xml.findall('source')
if sources:
model_data['html5_sources'] = [ele.get('src') for ele in sources]
model_data['source'] = model_data['html5_sources'][0]
track = xml.find('track')
if track is not None:
model_data['track'] = track.get('src')
for attr, value in xml.items():
if attr in video_compat:
attr = video_compat[attr]
if attr == 'youtube':
speeds = VideoAlphaDescriptor._parse_youtube(value)
for speed, youtube_id in speeds.items():
# should have made these youtube_id_1_00 for
# cleanliness, but hindsight doesn't need glasses
normalized_speed = speed[:-1] if speed.endswith('0') else speed
# If the user has specified html5 sources, make sure we don't use the default video
if youtube_id != '' or 'html5_sources' in model_data:
model_data['youtube_id_{0}'.format(normalized_speed.replace('.', '_'))] = youtube_id
else:
# Convert XML attrs into Python values.
if attr in conversions:
value = conversions[attr](value)
model_data[attr] = value
return model_data
@staticmethod
def _parse_time(str_time):
"""Converts s in '12:34:45' format to seconds. If s is
None, returns empty string"""
if not str_time:
return ''
else:
obj_time = time.strptime(str_time, '%H:%M:%S')
return datetime.timedelta(
hours=obj_time.tm_hour,
minutes=obj_time.tm_min,
seconds=obj_time.tm_sec
).total_seconds()
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]])
<chapter> <chapter>
<video url_name="toyvideo" youtube_id_1_0="OEoXaMPEzfM" display_name="toyvideo"/> <video url_name="toyvideo" youtube_id_1_0="OEoXaMPEzfMA" display_name="toyvideo"/>
</chapter> </chapter>
<video display_name="default" youtube_id_0_75="JMD_ifUUfsU" youtube_id_1_0="OEoXaMPEzfM" youtube_id_1_25="AKqURZnYqpk" youtube_id_1_5="DYpADpL7jAY" name="sample_video"/> <video display_name="default" youtube_id_0_75="JMD_ifUUfsU" youtube_id_1_0="OEoXaMPEzfM" youtube_id_1_25="AKqURZnYqpk" youtube_id_1_5="DYpADpL7jAY" name="sample_video"/>
\ No newline at end of file
Feature: Video component Feature: Video component
As a student, I want to view course videos in LMS. As a student, I want to view course videos in LMS.
Scenario: Autoplay is enabled in LMS for a Video component
Given the course has a Video component
Then when I view the video it has autoplay enabled
Scenario: Autoplay is enabled in the LMS for a VideoAlpha component Scenario: Video component is fully rendered in the LMS in HTML5 mode
Given the course has a VideoAlpha component Given the course has a Video component in HTML5 mode
Then when I view the videoalpha it has autoplay enabled Then when I view the video it has rendered in HTML5 mode
And all sources are correct
Scenario: Video component is fully rendered in the LMS in Youtube mode
Given the course has a Video component in Youtube mode
Then when I view the video it has rendered in Youtube mode
Scenario: Autoplay is enabled in LMS for a Video component
Given the course has a Video component in HTML5 mode
Then when I view the video it has autoplay enabled
\ No newline at end of file
...@@ -6,24 +6,24 @@ from common import i_am_registered_for_the_course, section_location ...@@ -6,24 +6,24 @@ from common import i_am_registered_for_the_course, section_location
############### ACTIONS #################### ############### ACTIONS ####################
HTML5_SOURCES = [
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp4',
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.webm',
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.ogv'
]
@step('when I view the video it has autoplay enabled') @step('when I view the (.*) it has autoplay enabled')
def does_autoplay_video(_step): def does_autoplay_video(_step, video_type):
assert(world.css_find('.video')[0]['data-autoplay'] == 'True') assert(world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'True')
@step('when I view the videoalpha it has autoplay enabled') @step('the course has a Video component in (.*) mode')
def does_autoplay_videoalpha(_step): def view_video(_step, player_mode):
assert(world.css_find('.videoalpha')[0]['data-autoplay'] == 'True')
@step('the course has a Video component')
def view_video(_step):
coursenum = 'test_course' coursenum = 'test_course'
i_am_registered_for_the_course(step, coursenum) i_am_registered_for_the_course(step, coursenum)
# Make sure we have a video # Make sure we have a video
add_video_to_course(coursenum) add_video_to_course(coursenum, player_mode.lower())
chapter_name = world.scenario_dict['SECTION'].display_name.replace(" ", "_") chapter_name = world.scenario_dict['SECTION'].display_name.replace(" ", "_")
section_name = chapter_name section_name = chapter_name
url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % url = django_url('/courses/%s/%s/%s/courseware/%s/%s' %
...@@ -32,29 +32,43 @@ def view_video(_step): ...@@ -32,29 +32,43 @@ def view_video(_step):
world.browser.visit(url) world.browser.visit(url)
@step('the course has a VideoAlpha component') def add_video_to_course(course, player_mode):
def view_videoalpha(step): category = 'video'
coursenum = 'test_course'
i_am_registered_for_the_course(step, coursenum) kwargs = {
'parent_location': section_location(course),
'category': category,
'display_name': 'Video'
}
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
}
})
world.ItemFactory.create(**kwargs)
# Make sure we have a videoalpha
add_videoalpha_to_course(coursenum)
chapter_name = world.scenario_dict['SECTION'].display_name.replace(" ", "_")
section_name = chapter_name
url = django_url('/courses/%s/%s/%s/courseware/%s/%s' %
(world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'),
chapter_name, section_name,))
world.browser.visit(url)
@step('when I view the video it has rendered in (.*) mode')
def video_is_rendered(_step, mode):
modes = {
'html5': 'video',
'youtube': 'iframe'
}
if mode.lower() in modes:
assert world.css_find('.video {0}'.format(modes[mode.lower()])).first
else:
assert False
def add_video_to_course(course): @step('all sources are correct')
world.ItemFactory.create(parent_location=section_location(course), def all_sources_are_correct(_step):
category='video', sources = world.css_find('.video video source')
display_name='Video') assert set(source['src'] for source in sources) == set(HTML5_SOURCES)
def add_videoalpha_to_course(course):
category = 'videoalpha'
world.ItemFactory.create(parent_location=section_location(course),
category=category,
display_name='Video Alpha')
...@@ -2,21 +2,35 @@ ...@@ -2,21 +2,35 @@
"""Video xmodule tests in mongo.""" """Video xmodule tests in mongo."""
from . import BaseTestXmodule from . import BaseTestXmodule
from .test_video_xml import SOURCE_XML
from django.conf import settings
from xmodule.video_module import _create_youtube_string
class TestVideo(BaseTestXmodule): class TestVideo(BaseTestXmodule):
"""Integration tests: web client + mongo.""" """Integration tests: web client + mongo."""
TEMPLATE_NAME = "video" CATEGORY = "video"
DATA = '<video youtube="0.75:JMD_ifUUfsU,1.0:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY"/>' DATA = SOURCE_XML
MODEL_DATA = {
'data': DATA
}
def setUp(self):
# Since the VideoDescriptor changes `self._model_data`,
# we need to instantiate `self.item_module` through
# `self.item_descriptor` rather than directly constructing it
super(TestVideo, self).setUp()
self.item_module = self.item_descriptor.xmodule(self.runtime)
self.item_module.runtime.render_template = lambda template, context: context
def test_handle_ajax_dispatch(self): def test_handle_ajax_dispatch(self):
responses = { responses = {
user.username: self.clients[user.username].post( user.username: self.clients[user.username].post(
self.get_url('whatever'), self.get_url('whatever'),
{}, {},
HTTP_X_REQUESTED_WITH='XMLHttpRequest' HTTP_X_REQUESTED_WITH='XMLHttpRequest')
) for user in self.users for user in self.users
} }
self.assertEqual( self.assertEqual(
...@@ -25,3 +39,82 @@ class TestVideo(BaseTestXmodule): ...@@ -25,3 +39,82 @@ class TestVideo(BaseTestXmodule):
for _, response in responses.items() for _, response in responses.items()
]).pop(), ]).pop(),
404) 404)
def test_video_constructor(self):
"""Make sure that all parameters extracted correclty from xml"""
context = self.item_module.get_html()
sources = {
'main': u'example.mp4',
u'mp4': u'example.mp4',
u'webm': u'example.webm',
u'ogv': u'example.ogv'
}
expected_context = {
'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': '/c4x/MITx/999/asset/subs_',
'show_captions': 'true',
'display_name': 'A Name',
'end': 3610.0,
'id': self.item_module.location.html_id(),
'sources': sources,
'start': 3603.0,
'sub': u'a_sub_file.srt.sjson',
'track': '',
'youtube_streams': _create_youtube_string(self.item_module),
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
}
self.maxDiff = None
self.assertEqual(context, expected_context)
class TestVideoNonYouTube(TestVideo):
"""Integration tests: web client + mongo."""
DATA = """
<video show_captions="true"
display_name="A Name"
sub="a_sub_file.srt.sjson"
start_time="01:00:03" end_time="01:00:10"
>
<source src="example.mp4"/>
<source src="example.webm"/>
<source src="example.ogv"/>
</video>
"""
MODEL_DATA = {
'data': DATA
}
def test_video_constructor(self):
"""Make sure that if the 'youtube' attribute is omitted in XML, then
the template generates an empty string for the YouTube streams.
"""
sources = {
u'main': u'example.mp4',
u'mp4': u'example.mp4',
u'webm': u'example.webm',
u'ogv': u'example.ogv'
}
context = self.item_module.get_html()
expected_context = {
'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': '/c4x/MITx/999/asset/subs_',
'show_captions': 'true',
'display_name': 'A Name',
'end': 3610.0,
'id': self.item_module.location.html_id(),
'sources': sources,
'start': 3603.0,
'sub': 'a_sub_file.srt.sjson',
'track': '',
'youtube_streams': '1.00:OEoXaMPEzfM',
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
}
self.assertEqual(context, expected_context)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Test for VideoAlpha Xmodule functional logic. # pylint: disable=W0212
"""Test for Video Xmodule functional logic.
These test data read from xml, not from mongo. These test data read from xml, not from mongo.
We have a ModuleStoreTestCase class defined in We have a ModuleStoreTestCase class defined in
...@@ -18,13 +20,14 @@ import unittest ...@@ -18,13 +20,14 @@ import unittest
from django.conf import settings from django.conf import settings
from xmodule.videoalpha_module import VideoAlphaDescriptor, _create_youtube_string from xmodule.video_module import (
VideoDescriptor, _create_youtube_string)
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.tests import get_test_system from xmodule.tests import get_test_system, LogicTest
SOURCE_XML = """ SOURCE_XML = """
<videoalpha show_captions="true" <video show_captions="true"
display_name="A Name" display_name="A Name"
youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg" youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg"
sub="a_sub_file.srt.sjson" sub="a_sub_file.srt.sjson"
...@@ -33,12 +36,12 @@ SOURCE_XML = """ ...@@ -33,12 +36,12 @@ SOURCE_XML = """
<source src="example.mp4"/> <source src="example.mp4"/>
<source src="example.webm"/> <source src="example.webm"/>
<source src="example.ogv"/> <source src="example.ogv"/>
</videoalpha> </video>
""" """
class VideoAlphaFactory(object): class VideoFactory(object):
"""A helper class to create videoalpha modules with various parameters """A helper class to create video modules with various parameters
for testing. for testing.
""" """
...@@ -47,28 +50,28 @@ class VideoAlphaFactory(object): ...@@ -47,28 +50,28 @@ class VideoAlphaFactory(object):
@staticmethod @staticmethod
def create(): def create():
"""Method return VideoAlpha Xmodule instance.""" """Method return Video Xmodule instance."""
location = Location(["i4x", "edX", "videoalpha", "default", location = Location(["i4x", "edX", "video", "default",
"SampleProblem1"]) "SampleProblem1"])
model_data = {'data': VideoAlphaFactory.sample_problem_xml_youtube, model_data = {'data': VideoFactory.sample_problem_xml_youtube,
'location': location} 'location': location}
system = get_test_system() system = get_test_system()
system.render_template = lambda template, context: context system.render_template = lambda template, context: context
descriptor = VideoAlphaDescriptor(system, model_data) descriptor = VideoDescriptor(system, model_data)
module = descriptor.xmodule(system) module = descriptor.xmodule(system)
return module return module
class VideoAlphaModuleUnitTest(unittest.TestCase): class VideoModuleUnitTest(unittest.TestCase):
"""Unit tests for VideoAlpha Xmodule.""" """Unit tests for Video Xmodule."""
def test_videoalpha_get_html(self): def test_video_get_html(self):
"""Make sure that all parameters extracted correclty from xml""" """Make sure that all parameters extracted correclty from xml"""
module = VideoAlphaFactory.create() module = VideoFactory.create()
module.runtime.render_template = lambda template, context: context module.runtime.render_template = lambda template, context: context
sources = { sources = {
...@@ -95,9 +98,77 @@ class VideoAlphaModuleUnitTest(unittest.TestCase): ...@@ -95,9 +98,77 @@ class VideoAlphaModuleUnitTest(unittest.TestCase):
self.assertEqual(module.get_html(), expected_context) self.assertEqual(module.get_html(), expected_context)
def test_videoalpha_instance_state(self): def test_video_instance_state(self):
module = VideoAlphaFactory.create() module = VideoFactory.create()
self.assertDictEqual( self.assertDictEqual(
json.loads(module.get_instance_state()), json.loads(module.get_instance_state()),
{'position': 0}) {'position': 0})
class VideoModuleLogicTest(LogicTest):
"""Tests for logic of Video Xmodule."""
descriptor_class = VideoDescriptor
raw_model_data = {
'data': '<video />'
}
def test_parse_time(self):
"""Ensure that times are parsed correctly into seconds."""
output = VideoDescriptor._parse_time('00:04:07')
self.assertEqual(output, 247)
def test_parse_time_none(self):
"""Check parsing of None."""
output = VideoDescriptor._parse_time(None)
self.assertEqual(output, '')
def test_parse_time_empty(self):
"""Check parsing of the empty string."""
output = VideoDescriptor._parse_time('')
self.assertEqual(output, '')
def test_parse_youtube(self):
"""Test parsing old-style Youtube ID strings into a dict."""
youtube_str = '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg'
output = VideoDescriptor._parse_youtube(youtube_str)
self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
'1.00': 'ZwkTiUPN0mg',
'1.25': 'rsq9auxASqI',
'1.50': 'kMyNdzVHHgg'})
def test_parse_youtube_one_video(self):
"""
Ensure that all keys are present and missing speeds map to the
empty string.
"""
youtube_str = '0.75:jNCf2gIqpeE'
output = VideoDescriptor._parse_youtube(youtube_str)
self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
'1.00': '',
'1.25': '',
'1.50': ''})
def test_parse_youtube_key_format(self):
"""
Make sure that inconsistent speed keys are parsed correctly.
"""
youtube_str = '1.00:p2Q6BrNhdh8'
youtube_str_hack = '1.0:p2Q6BrNhdh8'
self.assertEqual(
VideoDescriptor._parse_youtube(youtube_str),
VideoDescriptor._parse_youtube(youtube_str_hack)
)
def test_parse_youtube_empty(self):
"""
Some courses have empty youtube attributes, so we should handle
that well.
"""
self.assertEqual(VideoDescriptor._parse_youtube(''),
{'0.75': '',
'1.00': '',
'1.25': '',
'1.50': ''})
# -*- coding: utf-8 -*-
"""Video xmodule tests in mongo."""
from . import BaseTestXmodule
from .test_videoalpha_xml import SOURCE_XML
from django.conf import settings
from xmodule.videoalpha_module import _create_youtube_string
class TestVideo(BaseTestXmodule):
"""Integration tests: web client + mongo."""
CATEGORY = "videoalpha"
DATA = SOURCE_XML
MODEL_DATA = {
'data': DATA
}
def setUp(self):
# Since the VideoAlphaDescriptor changes `self._model_data`,
# we need to instantiate `self.item_module` through
# `self.item_descriptor` rather than directly constructing it
super(TestVideo, self).setUp()
self.item_module = self.item_descriptor.xmodule(self.runtime)
self.item_module.runtime.render_template = lambda template, context: context
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 test_videoalpha_constructor(self):
"""Make sure that all parameters extracted correclty from xml"""
context = self.item_module.get_html()
sources = {
'main': 'example.mp4',
'mp4': 'example.mp4',
'webm': 'example.webm',
'ogv': 'example.ogv'
}
expected_context = {
'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': '/c4x/MITx/999/asset/subs_',
'show_captions': 'true',
'display_name': 'A Name',
'end': 3610.0,
'id': self.item_module.location.html_id(),
'sources': sources,
'start': 3603.0,
'sub': 'a_sub_file.srt.sjson',
'track': '',
'youtube_streams': _create_youtube_string(self.item_module),
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
}
self.assertEqual(context, expected_context)
class TestVideoNonYouTube(TestVideo):
"""Integration tests: web client + mongo."""
DATA = """
<videoalpha show_captions="true"
display_name="A Name"
sub="a_sub_file.srt.sjson"
start_time="01:00:03" end_time="01:00:10"
>
<source src="example.mp4"/>
<source src="example.webm"/>
<source src="example.ogv"/>
</videoalpha>
"""
MODEL_DATA = {
'data': DATA
}
def test_videoalpha_constructor(self):
"""Make sure that if the 'youtube' attribute is omitted in XML, then
the template generates an empty string for the YouTube streams.
"""
sources = {
u'main': u'example.mp4',
u'mp4': u'example.mp4',
u'webm': u'example.webm',
u'ogv': u'example.ogv'
}
context = self.item_module.get_html()
expected_context = {
'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': '/c4x/MITx/999/asset/subs_',
'show_captions': 'true',
'display_name': 'A Name',
'end': 3610.0,
'id': self.item_module.location.html_id(),
'sources': sources,
'start': 3603.0,
'sub': 'a_sub_file.srt.sjson',
'track': '',
'youtube_streams': '1.00:OEoXaMPEzfM',
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
}
self.assertEqual(context, expected_context)
...@@ -75,10 +75,6 @@ XQUEUE_INTERFACE = { ...@@ -75,10 +75,6 @@ XQUEUE_INTERFACE = {
"basic_auth": ('anant', 'agarwal'), "basic_auth": ('anant', 'agarwal'),
} }
# Do not display the YouTube videos in the browser while running the
# acceptance tests. This makes them faster and more reliable
MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True
# Forums are disabled in test.py to speed up unit tests, but we do not have # Forums are disabled in test.py to speed up unit tests, but we do not have
# per-test control for acceptance tests # per-test control for acceptance tests
MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
......
...@@ -70,10 +70,6 @@ XQUEUE_INTERFACE = { ...@@ -70,10 +70,6 @@ XQUEUE_INTERFACE = {
"basic_auth": ('anant', 'agarwal'), "basic_auth": ('anant', 'agarwal'),
} }
# Do not display the YouTube videos in the browser while running the
# acceptance tests. This makes them faster and more reliable
MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
INSTALLED_APPS += ('lettuce.django',) INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('courseware',) LETTUCE_APPS = ('courseware',)
......
...@@ -86,8 +86,6 @@ MITX_FEATURES = { ...@@ -86,8 +86,6 @@ MITX_FEATURES = {
'DISABLE_LOGIN_BUTTON': False, # used in systems where login is automatic, eg MIT SSL 'DISABLE_LOGIN_BUTTON': False, # used in systems where login is automatic, eg MIT SSL
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
# extrernal access methods # extrernal access methods
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False, 'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
'AUTH_USE_OPENID': False, 'AUTH_USE_OPENID': False,
......
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
% if display_name is not UNDEFINED and display_name is not None: % if display_name is not UNDEFINED and display_name is not None:
<h2> ${display_name} </h2> <h2>${display_name}</h2>
% endif % endif
%if settings.MITX_FEATURES.get('USE_YOUTUBE_OBJECT_API') and normal_speed_video_id: <div
<object width="640" height="390"> id="video_${id}"
<param name="movie" class="video"
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
value="https://www.youtube.com/v/${normal_speed_video_id}?version=3&amp;autoplay=1&amp;rel=0"> data-streams="${youtube_streams}"
% endif
</param> ${'data-sub="{}"'.format(sub) if sub else ''}
<param name="allowScriptAccess" value="always"></param> ${'data-autoplay="{}"'.format(autoplay) if autoplay else ''}
<embed
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: ${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''}
src="https://www.youtube.com/v/${normal_speed_video_id}?version=3&amp;autoplay=1&amp;rel=0" ${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''}
% endif ${'data-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''}
type="application/x-shockwave-flash"
allowscriptaccess="always" data-caption-data-dir="${data_dir}"
width="640" height="390"></embed> data-show-captions="${show_captions}"
</object> data-start="${start}"
%else: data-end="${end}"
<div id="video_${id}" data-caption-asset-path="${caption_asset_path}"
class="video" data-autoplay="${autoplay}"
data-youtube-id-0-75="${youtube_id_0_75}" >
data-youtube-id-1-0="${youtube_id_1_0}"
data-youtube-id-1-25="${youtube_id_1_25}"
data-youtube-id-1-5="${youtube_id_1_5}"
data-show-captions="${show_captions}"
data-start="${start}"
data-end="${end}"
data-caption-asset-path="${caption_asset_path}"
data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}">
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<section class="video-player"> <div class="video-player-pre"></div>
<div id="${id}"></div>
</section> <section class="video-player">
<section class="video-controls"></section> <div id="${id}"></div>
</article> </section>
</div>
</div> <div class="video-player-post"></div>
%endif
<section class="video-controls">
<div class="slider" tabindex="0" title="Video position"></div>
% if source: <div>
<div class="video-sources"> <ul class="vcr">
<p>${_('Download video <a href="%s">here</a>.') % source}</p> <li><a class="video_control" href="#" tabindex="0" title="${_('Play')}"></a></li>
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#" tabindex="0" title="Speeds">
<h3 tabindex="-1">${_('Speed')}</h3>
<p tabindex="-1" class="active"></p>
</a>
<ol tabindex="-1" class="video_speeds"></ol>
</div>
<div class="volume">
<a href="#" tabindex="0" title="Volume"></a>
<div tabindex="-1" class="volume-slider-container">
<div tabindex="-1" class="volume-slider"></div>
</div>
</div>
<a href="#" class="add-fullscreen" tabindex="0" title="${_('Fill browser')}">${_('Fill browser')}</a>
<a href="#" class="quality_control" tabindex="0" title="${_('HD')}">${_('HD')}</a>
<a href="#" class="hide-subtitles" title="${_('Turn off captions')}">${_('Captions')}</a>
</div>
</div>
</section>
</article>
<ol class="subtitles" tabindex="0" title="Captions"><li></li></ol>
</div>
</div> </div>
% if sources.get('main'):
<div class="video-sources">
<p>${(_('Download video') + ' <a href="%s">' + _('here') + '</a>.') % sources.get('main')}</p>
</div>
% endif % endif
% if track: % if track:
<div class="video-tracks"> <div class="video-tracks">
<p>${_('Download subtitles <a href="%s">here</a>.') % track}</p> <p>${(_('Download subtitles') + ' <a href="%s">' + _('here') + '</a>.') % track}</p>
</div> </div>
% endif % endif
<%! from django.utils.translation import ugettext as _ %>
% if display_name is not UNDEFINED and display_name is not None:
<h2>${display_name}</h2>
% endif
<div
id="video_${id}"
class="videoalpha"
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
data-streams="${youtube_streams}"
% endif
${'data-sub="{}"'.format(sub) if sub else ''}
${'data-autoplay="{}"'.format(autoplay) if autoplay else ''}
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''}
${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''}
${'data-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''}
% endif
data-caption-data-dir="${data_dir}"
data-show-captions="${show_captions}"
data-start="${start}"
data-end="${end}"
data-caption-asset-path="${caption_asset_path}"
data-autoplay="${autoplay}"
>
<div class="tc-wrapper">
<article class="video-wrapper">
<div class="video-player-pre"></div>
<section class="video-player">
<div id="${id}"></div>
</section>
<div class="video-player-post"></div>
<section class="video-controls">
<div class="slider" tabindex="0" title="Video position"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#" tabindex="0" title="${_('Play')}"></a></li>
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#" tabindex="0" title="Speeds">
<h3 tabindex="-1">${_('Speed')}</h3>
<p tabindex="-1" class="active"></p>
</a>
<ol tabindex="-1" class="video_speeds"></ol>
</div>
<div class="volume">
<a href="#" tabindex="0" title="Volume"></a>
<div tabindex="-1" class="volume-slider-container">
<div tabindex="-1" class="volume-slider"></div>
</div>
</div>
<a href="#" class="add-fullscreen" tabindex="0" title="${_('Fill browser')}">${_('Fill browser')}</a>
<a href="#" class="quality_control" tabindex="0" title="${_('HD')}">${_('HD')}</a>
<a href="#" class="hide-subtitles" title="${_('Turn off captions')}">${_('Captions')}</a>
</div>
</div>
</section>
</article>
<ol class="subtitles" tabindex="0" title="Captions"><li></li></ol>
</div>
</div>
% if sources.get('main'):
<div class="video-sources">
<p>${(_('Download video') + ' <a href="%s">' + _('here') + '</a>.') % sources.get('main')}</p>
</div>
% endif
% if track:
<div class="video-tracks">
<p>${(_('Download subtitles') + ' <a href="%s">' + _('here') + '</a>.') % track}</p>
</div>
% endif
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment