Commit 4c7bfb44 by Alexander Kryklia

Add Video Bumper.

Fix n-click behaviour on poster.
Fix unit tests.
Fix handler for non_en lang for bumper.
Add more tests.
Fix docstrings.
Fix pep8.
Fix static redirection with bumper.
Fix button in IE11.
Add video_bumper field in bok_choy.

Fix pylink violations.

Update docstrings and some clean up.

Rename edx_video_id in bumper tests.

Fix too long lines in help text.

Address ui comments.

Fix bumper events.

Refactor bumper-transcripts code, fix bugs, address comments.
Squashed commits:
Fix download transcript button.
[74e0c8c] Fix quality
[a759f33] Fix error, when sub contains extension.
[b30755c] Revert "Add video files to host for transcripts."

This reverts commit cf8a96bf84346e17b6ad57ad4cc6a27d7a9118cd.
[36f038a] Add video files to host for transcripts.
[23f1655] Fix pep8 and pyling issues.
[0f1f9d2] Update acceptance test.
[765a27d] Wait for ajax in captions.
[8ae72a3] Fix logic.
[063450f] Fix unit tests.
[d1075fc] Fix handlers tests.
[25d31ad] Update bumper_utils.
[cb5f9df] Remove maxDiff.
[8738b1a] Code cleanup.
[87dbcb7] Fix issues with transcripts.
[ec899de] Fix transcripts in serializers.
[444b1fc] Fix transcripts typo.
[d524cb5] Fix bumper.
[f62cf22] Fix video mongo tests.
[8f1b55a] Fix dispatches.
[53bc308] Add more fixes.
[d5e3723] Fix test_video_handlers and rename the method.
[93efc23] Fix mobile tests.
[740e2ae] Fix pep8 and pylint.
[47cfb66] Address comments, add fixes.
[4e499d9] Add fixes.
[8353553] Add improvements.

Updated dispatch values)
.

Use ddt in bumper handler tests.

Move common metadata to single place.

Fix style.

Update docstring.

Fix poster button.

Improve bumper events.

Fix test after rebase.

Address comments.

Download transcript: use def video lang, not bump.

Renamed date_last_view_bumper to bumper_last_view_date.

Rename do_not_show_again_bumper to bumper_...

Address comments.

Fix tests for download for en lang.

Fix bumper logic.

Update strings.

Update resizer.

Remove resizer.

Fix unit tests.

Add tests.

Fix bumper events.

Clean up tests.

Fix pylint violations.

Fix pep8 and pylint violations.

Update docs and method names.

Update events.

Make /static/ prefix a must.

Fix wrong code.
parent 93faba00
...@@ -78,6 +78,9 @@ class CourseMetadata(object): ...@@ -78,6 +78,9 @@ class CourseMetadata(object):
if not settings.FEATURES.get('ENABLE_TEAMS'): if not settings.FEATURES.get('ENABLE_TEAMS'):
filtered_list.append('teams_configuration') filtered_list.append('teams_configuration')
if not settings.FEATURES.get('ENABLE_VIDEO_BUMPER'):
filtered_list.append('video_bumper')
return filtered_list return filtered_list
@classmethod @classmethod
......
...@@ -74,6 +74,9 @@ FEATURES['ENABLE_TEAMS'] = True ...@@ -74,6 +74,9 @@ FEATURES['ENABLE_TEAMS'] = True
# Enable custom content licensing # Enable custom content licensing
FEATURES['LICENSING'] = True FEATURES['LICENSING'] = True
FEATURES['ENABLE_MOBILE_REST_API'] = True # Enable video bumper in Studio
FEATURES['ENABLE_VIDEO_BUMPER'] = True # Enable video bumper in Studio settings
########################### Entrance Exams ################################# ########################### Entrance Exams #################################
FEATURES['ENTRANCE_EXAMS'] = True FEATURES['ENTRANCE_EXAMS'] = True
......
...@@ -163,6 +163,13 @@ FEATURES = { ...@@ -163,6 +163,13 @@ FEATURES = {
# Teams feature # Teams feature
'ENABLE_TEAMS': False, 'ENABLE_TEAMS': False,
# Show video bumper in Studio
'ENABLE_VIDEO_BUMPER': False,
# How many seconds to show the bumper again, default is 7 days:
'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600,
} }
ENABLE_JASMINE = False ENABLE_JASMINE = False
...@@ -645,6 +652,8 @@ YOUTUBE = { ...@@ -645,6 +652,8 @@ YOUTUBE = {
'v': 'set_youtube_id_of_11_symbols_here', 'v': 'set_youtube_id_of_11_symbols_here',
}, },
}, },
'IMAGE_API': 'http://img.youtube.com/vi/{youtube_id}/0.jpg', # /maxresdefault.jpg for 1920*1080
} }
############################# VIDEO UPLOAD PIPELINE ############################# ############################# VIDEO UPLOAD PIPELINE #############################
......
...@@ -22,7 +22,7 @@ $a11y--blue-s1: saturate($blue,15%); ...@@ -22,7 +22,7 @@ $a11y--blue-s1: saturate($blue,15%);
} }
.a11y-menu-list { .a11y-menu-list {
@extend %ui-depth1; @extend %ui-depth3;
top: 100%; top: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
......
...@@ -27,6 +27,23 @@ div.video { ...@@ -27,6 +27,23 @@ div.video {
} }
} }
// CASE: video pre-roll state
&.is-pre-roll {
.slider {
visibility: hidden;
}
.video-player {
position: relative;
&:before {
display: block;
content: "";
width: 100%;
padding-top: 55%;
}
}
}
div.tc-wrapper { div.tc-wrapper {
@include clearfix(); @include clearfix();
position: relative; position: relative;
...@@ -169,6 +186,7 @@ div.video { ...@@ -169,6 +186,7 @@ div.video {
} }
object, iframe, video { object, iframe, video {
display: block;
border: none; border: none;
width: 100%; width: 100%;
} }
...@@ -282,7 +300,7 @@ div.video { ...@@ -282,7 +300,7 @@ div.video {
} }
} }
ul.vcr { .vcr {
float: left; float: left;
list-style: none; list-style: none;
margin: 0 lh() 0 0; margin: 0 lh() 0 0;
...@@ -293,49 +311,52 @@ div.video { ...@@ -293,49 +311,52 @@ div.video {
font-size: em(14); font-size: em(14);
} }
li { .video_control {
@extend %video-button;
float: left; float: left;
margin-bottom: 0; background-image: url('../images/vcr.png');
background-position: 15px 15px ;
background-repeat: no-repeat;
border-left: none;
padding: 0 lh(.75);
width: 14px;
a { &:focus {
@extend %video-button; @extend %ui-depth4;
background-image: url('../images/vcr.png'); position: relative;
background-position: 15px 15px ; outline: $white dotted thin;
background-repeat: no-repeat; outline-offset: -2px;
border-left: none; }
box-shadow: 1px 0 0 #555;
padding: 0 lh(.75);
width: 14px;
&:focus {
@extend %ui-depth4;
position: relative;
outline: $white dotted thin;
outline-offset: -2px;
}
&:empty { &:empty {
height: 46px; height: 46px;
background-position: 15px 15px; background-position: 15px 15px;
} }
&.play { &.play {
background-position: 17px -114px; background-position: 17px -114px;
} }
&.pause { &.pause {
background-position: 16px -50px; background-position: 16px -50px;
}
} }
div.vidtime { &.skip {
font-weight: bold; background-image: none;
line-height: 46px; //height of play pause buttons text-indent: 0;
-webkit-font-smoothing: antialiased; width: initial;
padding-left: lh(.75); white-space: nowrap;
@media (max-width: 1120px) { }
padding-left: lh(0.5); }
}
div.vidtime {
@extend %t-strong;
float: left;
line-height: 46px; //height of play pause buttons
-webkit-font-smoothing: antialiased;
padding-left: lh(.75);
@media (max-width: 1120px) {
padding-left: lh(0.5);
} }
} }
} }
...@@ -504,11 +525,14 @@ div.video { ...@@ -504,11 +525,14 @@ div.video {
background-image: url('../images/volume.png'); background-image: url('../images/volume.png');
background-position: 10px center; background-position: 10px center;
background-repeat: no-repeat; background-repeat: no-repeat;
border-left: none;
width: 30px; width: 30px;
height: 46px; height: 46px;
} }
&:not(:first-child) > a {
border-left: none;
}
.volume-slider-container { .volume-slider-container {
@include transition(none); @include transition(none);
@extend %ui-depth1; @extend %ui-depth1;
...@@ -686,8 +710,7 @@ div.video { ...@@ -686,8 +710,7 @@ div.video {
} }
ol.subtitles { ol.subtitles {
width: 0; @extend .is-hidden;
height: 0;
} }
ol.subtitles.html5 { ol.subtitles.html5 {
...@@ -792,13 +815,38 @@ div.video { ...@@ -792,13 +815,38 @@ div.video {
&.is-touch { &.is-touch {
div.tc-wrapper { div.tc-wrapper {
article.video-wrapper { article.video-wrapper {
object, iframe, video{ object, iframe, video {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
} }
} }
} }
.video-pre-roll {
@extend %ui-depth3;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: 100%;
background-color: $black;
&.is-html5 {
background-size: 15%;
}
.btn-play {
text-indent: -999px;
overflow: hidden;
border: none;
box-shadow: none;
line-height: 0;
}
}
} }
...@@ -4,22 +4,7 @@ ...@@ -4,22 +4,7 @@
<div <div
id="video_id" id="video_id"
class="video closed" class="video closed"
data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl" data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": "[]", "speed": "1.5", "startTime": "", "streams": "0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}'
data-show-captions="true"
data-save-state-url="/save_user_state"
data-speed="1.5"
data-start=""
data-end=""
data-saved-video-position="0"
data-transcript-language="en"
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-api-url="www.youtube.com/iframe_api"
data-yt-test-url="gdata.youtube.com/feeds/api/videos/"
data-autohide-html5="True"
> >
<div class="focus_grabber first"></div> <div class="focus_grabber first"></div>
...@@ -35,35 +20,11 @@ ...@@ -35,35 +20,11 @@
<section class="video-controls is-hidden"> <section class="video-controls is-hidden">
<div class="slider"></div> <div class="slider"></div>
<div> <div>
<ul class="vcr"> <div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
<li><a class="video_control" href="#" title="Play" role="button" aria-disabled="false"></a></li> <div class="secondary-controls"></div>
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false">
<span class="label">Speed</span>
<span class="value"></span>
</a>
<ol class="video-speeds"></ol>
</div>
<div class="volume">
<a href="#" title="Volume" role="button" aria-disabled="false"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
<a href="#" class="quality-control is-hidden" title="HD off" role="button" aria-disabled="false">HD off</a>
<div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div>
</div>
</div> </div>
</section> </section>
</article> </article>
<ol class="subtitles"><li></li></ol>
</div> </div>
<div class="focus_grabber last"></div> <div class="focus_grabber last"></div>
......
...@@ -4,23 +4,7 @@ ...@@ -4,23 +4,7 @@
<div <div
id="video_id" id="video_id"
class="video closed" class="video closed"
data-show-captions="true" data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}'
data-save-state-url="/save_user_state"
data-speed="1.5"
data-start=""
data-end=""
data-saved-video-position="0"
data-transcript-language="en"
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations"
data-sub="Z5KLxerq05Y"
data-sources='["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"]'
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-api-url="www.youtube.com/iframe_api"
data-yt-test-url="gdata.youtube.com/feeds/api/videos/"
data-autohide-html5="True"
> >
<div class="focus_grabber first"></div> <div class="focus_grabber first"></div>
...@@ -36,35 +20,11 @@ ...@@ -36,35 +20,11 @@
<section class="video-controls is-hidden"> <section class="video-controls is-hidden">
<div class="slider"></div> <div class="slider"></div>
<div> <div>
<ul class="vcr"> <div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
<li><a class="video_control" href="#" title="Play" role="button" aria-disabled="false"></a></li> <div class="secondary-controls"></div>
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false">
<span class="label">Speed</span>
<span class="value"></span>
</a>
<ol class="video-speeds"></ol>
</div>
<div class="volume">
<a href="#" title="Volume" role="button" aria-disabled="false"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
<a href="#" class="quality-control is-hidden" title="HD off" role="button" aria-disabled="false">HD off</a>
<div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div>
</div>
</div> </div>
</section> </section>
</article> </article>
<ol class="subtitles"><li></li></ol>
</div> </div>
<div class="focus_grabber last"></div> <div class="focus_grabber last"></div>
......
...@@ -4,23 +4,7 @@ ...@@ -4,23 +4,7 @@
<div <div
id="video_id" id="video_id"
class="video closed" class="video closed"
data-show-captions="true" data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/", "source": "", "html5_sources": ["http://youtu.be/3_yD_cEKoCk.mp4"]}'
data-save-state-url="/save_user_state"
data-speed="1.5"
data-start=""
data-end=""
data-saved-video-position="0"
data-transcript-language="en"
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations"
data-sub="Z5KLxerq05Y"
data-sources='["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"]'
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-api-url="www.youtube.com/iframe_api"
data-yt-test-url="gdata.youtube.com/feeds/api/videos/"
data-autohide-html5="True"
> >
<div class="focus_grabber first"></div> <div class="focus_grabber first"></div>
...@@ -33,8 +17,6 @@ ...@@ -33,8 +17,6 @@
</section> </section>
<section class="video-controls is-hidden"></section> <section class="video-controls is-hidden"></section>
</article> </article>
<ol class="subtitles"><li></li></ol>
</div> </div>
<div class="focus_grabber last"></div> <div class="focus_grabber last"></div>
......
...@@ -4,22 +4,7 @@ ...@@ -4,22 +4,7 @@
<div <div
id="video_id" id="video_id"
class="video closed" class="video closed"
data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl" data-metadata='{"streams":"0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl", "showCaptions": false, "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "speed": "1.5", "startTime": "", "end": "", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}'
data-show-captions="false"
data-save-state-url="/save_user_state"
data-speed="1.5"
data-start=""
data-end=""
data-saved-video-position="0"
data-transcript-language="en"
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-api-url="www.youtube.com/iframe_api"
data-yt-test-url="gdata.youtube.com/feeds/api/videos/"
data-autohide-html5="True"
> >
<div class="focus_grabber first"></div> <div class="focus_grabber first"></div>
......
<div class="course-content">
<div id="video_example">
<div id="example">
<div
id="video_id"
class="video closed"
data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}'
data-bumper-metadata='{"transcriptLanguage": "en", "showCaptions": "true", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "transcriptTranslationUrl": "/transcript/translation/__lang__/?is_bumper=1", "transcriptAvailableTranslationsUrl": "/transcript/available_translations/?is_bumper=1", "streams": "", "saveStateUrl": "/save_user_state"}'
data-poster='{"url": "xmodule/include/fixtures/poster.jpg", "type": "youtube"}'
>
<div class="focus_grabber first"></div>
<div class="tc-wrapper">
<article class="video-wrapper">
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
<span tabindex="-1" class="btn-play is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
<div class="video-player-pre"></div>
<section class="video-player">
<iframe id="id"></iframe>
</section>
<div class="video-player-post"></div>
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
<div class="secondary-controls"></div>
</div>
</section>
</article>
</div>
<div class="focus_grabber last"></div>
</div>
</div>
</div>
</div>
...@@ -4,22 +4,7 @@ ...@@ -4,22 +4,7 @@
<div <div
id="video_id1" id="video_id1"
class="video closed" class="video closed"
data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl" data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl", "sub": "", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}'
data-show-captions="true"
data-save-state-url="/save_user_state"
data-speed="1.5"
data-start=""
data-end=""
data-saved-video-position="0"
data-transcript-language="en"
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-api-url="www.youtube.com/iframe_api"
data-yt-test-url="gdata.youtube.com/feeds/api/videos/"
data-autohide-html5="True"
> >
<div class="focus_grabber first"></div> <div class="focus_grabber first"></div>
...@@ -35,35 +20,11 @@ ...@@ -35,35 +20,11 @@
<section class="video-controls is-hidden"> <section class="video-controls is-hidden">
<div class="slider"></div> <div class="slider"></div>
<div> <div>
<ul class="vcr"> <div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
<li><a class="video_control" href="#" title="Play" role="button" aria-disabled="false"></a></li> <div class="secondary-controls"></div>
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false">
<span class="label">Speed</span>
<span class="value"></span>
</a>
<ol class="video-speeds"></ol>
</div>
<div class="volume">
<a href="#" title="Volume" role="button" aria-disabled="false"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
<a href="#" class="quality-control is-hidden" title="HD off" role="button" aria-disabled="false">HD off</a>
<div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div>
</div>
</div> </div>
</section> </section>
</article> </article>
<ol class="subtitles"><li></li></ol>
</div> </div>
<div class="focus_grabber last"></div> <div class="focus_grabber last"></div>
...@@ -77,20 +38,7 @@ ...@@ -77,20 +38,7 @@
<div <div
id="video_id2" id="video_id2"
class="video" class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM" data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.0", "startTime": "", "streams": "0.75:7tqY6eQzVhE,1.0:cogebirgzzM", "sub": "", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}'
data-show-captions="true"
data-speed="1.0"
data-start=""
data-end=""
data-transcript-language="en"
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
data-autohide-html5="True"
> >
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
...@@ -102,30 +50,8 @@ ...@@ -102,30 +50,8 @@
<section class="video-controls"> <section class="video-controls">
<div class="slider"></div> <div class="slider"></div>
<div> <div>
<ul class="vcr"> <div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
<li><a class="video_control" href="#" title="Play"></a></li> <div class="secondary-controls"></div>
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false">
<span class="label">Speed</span>
<span class="value"></span>
</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 is-hidden" title="HD">HD</a>
<div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div>
</div>
</div> </div>
</section> </section>
</article> </article>
...@@ -142,20 +68,7 @@ ...@@ -142,20 +68,7 @@
<div <div
id="video_id3" id="video_id3"
class="video" class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM" data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.0", "startTime": "", "streams": "0.75:7tqY6eQzVhE,1.0:cogebirgzzM", "sub": "", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}'
data-show-captions="true"
data-speed="1.0"
data-start=""
data-end=""
data-transcript-language="en"
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
data-autohide-html5="True"
> >
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
......
...@@ -206,6 +206,9 @@ ...@@ -206,6 +206,9 @@
}, },
toBeInArray: function (array) { toBeInArray: function (array) {
return $.inArray(this.actual, array) > -1; return $.inArray(this.actual, array) > -1;
},
toBeFocused: function () {
return $(this.actual)[0] === $(this.actual)[0].ownerDocument.activeElement;
} }
}); });
...@@ -239,12 +242,11 @@ ...@@ -239,12 +242,11 @@
loadFixtures('video_all.html'); loadFixtures('video_all.html');
} }
// If `params` is an object, assign it's properties as data attributes // If `params` is an object, assign its properties as data attributes
// to the main video DIV element. // to the main video DIV element.
if (_.isObject(params)) { if (_.isObject(params)) {
$('#example') var metadata = _.extend($('#video_id').data('metadata'), params);
.find('#video_id') $('#video_id').data('metadata', metadata);
.data(params);
} }
jasmine.stubRequests(); jasmine.stubRequests();
......
(function (undefined) { (function (undefined) {
describe('Video', function () { describe('Video', function () {
var oldOTBD; var oldOTBD, state;
beforeEach(function () { beforeEach(function () {
jasmine.stubRequests(); jasmine.stubRequests();
...@@ -17,11 +17,12 @@ ...@@ -17,11 +17,12 @@
beforeEach(function () { beforeEach(function () {
loadFixtures('video.html'); loadFixtures('video.html');
$.cookie.andReturn('0.50'); $.cookie.andReturn('0.50');
this.state = jasmine.initializePlayerYouTube('video_html5.html');
}); });
describe('by default', function () { describe('by default', function () {
beforeEach(function () { afterEach(function () {
this.state = new window.Video('#example'); this.state.videoPlayer.destroy();
}); });
it('check videoType', function () { it('check videoType', function () {
...@@ -54,19 +55,16 @@ ...@@ -54,19 +55,16 @@
var state; var state;
beforeEach(function () { beforeEach(function () {
loadFixtures('video_html5.html');
$.cookie.andReturn('0.75'); $.cookie.andReturn('0.75');
state = jasmine.initializePlayer('video_html5.html');
}); });
describe('by default', function () { afterEach(function () {
beforeEach(function () { state.videoPlayer.destroy();
state = new window.Video('#example'); state = undefined;
}); });
afterEach(function () {
state = undefined;
});
describe('by default', function () {
it('check videoType', function () { it('check videoType', function () {
expect(state.videoType).toEqual('html5'); expect(state.videoType).toEqual('html5');
}); });
...@@ -95,14 +93,6 @@ ...@@ -95,14 +93,6 @@
// the stand alone HTML5 player object is already loaded, so no // the stand alone HTML5 player object is already loaded, so no
// further testing in that case is required. // further testing in that case is required.
describe('HTML5 API is available', function () { describe('HTML5 API is available', function () {
beforeEach(function () {
state = new Video('#example');
});
afterEach(function () {
state = null;
});
it('create the Video Player', function () { it('create the Video Player', function () {
expect(state.videoPlayer.player).not.toBeUndefined(); expect(state.videoPlayer.player).not.toBeUndefined();
}); });
...@@ -113,8 +103,11 @@ ...@@ -113,8 +103,11 @@
describe('YouTube API is not loaded', function () { describe('YouTube API is not loaded', function () {
beforeEach(function () { beforeEach(function () {
window.YT = undefined; window.YT = undefined;
state = jasmine.initializePlayerYouTube();
})
state = jasmine.initializePlayerYouTube('video.html'); afterEach(function () {
state.videoPlayer.destroy();
}); });
it('callback, to be called after YouTube API loads, exists and is called', function () { it('callback, to be called after YouTube API loads, exists and is called', function () {
...@@ -159,9 +152,8 @@ ...@@ -159,9 +152,8 @@
} }
]; ];
beforeEach(function () { afterEach(function () {
loadFixtures('video.html'); state.videoPlayer.destroy();
}); });
$.each(miniTestSuite, function (index, test) { $.each(miniTestSuite, function (index, test) {
...@@ -172,13 +164,10 @@ ...@@ -172,13 +164,10 @@
function itFabrique(itDescription, data, expectData) { function itFabrique(itDescription, data, expectData) {
it(itDescription, function () { it(itDescription, function () {
$('#example').find('.video') state = jasmine.initializePlayer('video.html', {
.data({ 'start': data.start,
'start': data.start, 'end': data.end
'end': data.end });
});
state = new Video('#example');
expect(state.config.startTime).toBe(expectData.start); expect(state.config.startTime).toBe(expectData.start);
expect(state.config.endTime).toBe(expectData.end); expect(state.config.endTime).toBe(expectData.end);
...@@ -238,26 +227,5 @@ ...@@ -238,26 +227,5 @@
expect(numAjaxCalls).toBe(1); expect(numAjaxCalls).toBe(1);
}); });
}); });
describe('log', function () {
beforeEach(function () {
loadFixtures('video_html5.html');
state = new Video('#example');
spyOn(Logger, 'log');
state.videoPlayer.log('someEvent', {
currentTime: 25,
speed: '1.0'
});
});
it('call the logger with valid extra parameters', function () {
expect(Logger.log).toHaveBeenCalledWith('someEvent', {
id: 'id',
code: 'html5',
currentTime: 25,
speed: '1.0'
});
});
});
}); });
}).call(this); }).call(this);
...@@ -10,8 +10,8 @@ ...@@ -10,8 +10,8 @@
afterEach(function () { afterEach(function () {
state.storage.clear(); state.storage.clear();
state.videoPlayer.destroy();
$.fn.scrollTo.reset(); $.fn.scrollTo.reset();
$('.subtitles').remove();
$('source').remove(); $('source').remove();
window.onTouchBasedDevice = oldOTBD; window.onTouchBasedDevice = oldOTBD;
}); });
......
...@@ -12,158 +12,6 @@ function (Initialize) { ...@@ -12,158 +12,6 @@ function (Initialize) {
state = {}; state = {};
}); });
describe('saveState function', function () {
var videoPlayerCurrentTime, newCurrentTime, speed;
// We make sure that `currentTime` is a float. We need to test
// that Math.round() is called.
videoPlayerCurrentTime = 3.1242;
// We have two times, because one is stored in
// `videoPlayer.currentTime`, and the other is passed directly to
// `saveState` in `data` object. In each case, there is different
// code that handles these times. They have to be different for
// test completeness sake. Also, make sure it is float, as is the
// time above.
newCurrentTime = 5.4;
speed = '0.75';
beforeEach(function () {
state = {
videoPlayer: {
currentTime: videoPlayerCurrentTime
},
storage: {
setItem: jasmine.createSpy()
},
config: {
saveStateUrl: 'http://example.com/save_user_state'
}
};
spyOn($, 'ajax');
spyOn(Time, 'formatFull').andCallThrough();
});
it('data is not an object, async is true', function () {
itSpec({
asyncVal: true,
speedVal: undefined,
positionVal: videoPlayerCurrentTime,
data: undefined,
ajaxData: {
saved_video_position: Time.formatFull(Math.round(videoPlayerCurrentTime))
}
});
});
it('data contains speed, async is false', function () {
itSpec({
asyncVal: false,
speedVal: speed,
positionVal: undefined,
data: {
speed: speed
},
ajaxData: {
speed: speed
}
});
});
it('data contains float position, async is true', function () {
itSpec({
asyncVal: true,
speedVal: undefined,
positionVal: newCurrentTime,
data: {
saved_video_position: newCurrentTime
},
ajaxData: {
saved_video_position: Time.formatFull(Math.round(newCurrentTime))
}
});
});
it('data contains speed and rounded position, async is false', function () {
itSpec({
asyncVal: false,
speedVal: speed,
positionVal: Math.round(newCurrentTime),
data: {
speed: speed,
saved_video_position: Math.round(newCurrentTime)
},
ajaxData: {
speed: speed,
saved_video_position: Time.formatFull(Math.round(newCurrentTime))
}
});
});
it('data contains empty object, async is true', function () {
itSpec({
asyncVal: true,
speedVal: undefined,
positionVal: undefined,
data: {},
ajaxData: {}
});
});
it('data contains position 0, async is true', function () {
itSpec({
asyncVal: true,
speedVal: undefined,
positionVal: 0,
data: {
saved_video_position: 0
},
ajaxData: {
saved_video_position: Time.formatFull(Math.round(0))
}
});
});
return;
function itSpec(value) {
var asyncVal = value.asyncVal,
speedVal = value.speedVal,
positionVal = value.positionVal,
data = value.data,
ajaxData = value.ajaxData;
Initialize.prototype.saveState.call(state, asyncVal, data);
if (speedVal) {
expect(state.storage.setItem).toHaveBeenCalledWith(
'speed',
speedVal,
true
);
}
if (positionVal) {
expect(state.storage.setItem).toHaveBeenCalledWith(
'savedVideoPosition',
positionVal,
true
);
expect(Time.formatFull).toHaveBeenCalledWith(
positionVal
);
}
expect($.ajax).toHaveBeenCalledWith({
url: state.config.saveStateUrl,
type: 'POST',
async: asyncVal,
dataType: 'json',
data: ajaxData
});
}
});
describe('getCurrentLanguage', function () { describe('getCurrentLanguage', function () {
var msg; var msg;
...@@ -356,20 +204,12 @@ function (Initialize) { ...@@ -356,20 +204,12 @@ function (Initialize) {
describe('when new speed is available', function () { describe('when new speed is available', function () {
beforeEach(function () { beforeEach(function () {
Initialize.prototype.setSpeed.call(state, '0.75', true); Initialize.prototype.setSpeed.call(state, '0.75');
}); });
it('set new speed', function () { it('set new speed', function () {
expect(state.speed).toEqual('0.75'); expect(state.speed).toEqual('0.75');
}); });
it('save setting for new speed', function () {
expect(state.storage.setItem.calls[0].args)
.toEqual(['speed', '0.75', true]);
expect(state.storage.setItem.calls[1].args)
.toEqual(['general_speed', '0.75']);
});
}); });
describe('when new speed is not available', function () { describe('when new speed is not available', function () {
...@@ -390,7 +230,7 @@ function (Initialize) { ...@@ -390,7 +230,7 @@ function (Initialize) {
}; };
$.each(map, function(key, expected) { $.each(map, function(key, expected) {
Initialize.prototype.setSpeed.call(state, key, true); Initialize.prototype.setSpeed.call(state, key);
expect(state.speed).toBe(expected); expect(state.speed).toBe(expected);
}); });
}); });
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
afterEach(function () { afterEach(function () {
$('source').remove(); $('source').remove();
state.storage.clear(); state.storage.clear();
state.videoPlayer.destroy();
}); });
describe('constructor', function () { describe('constructor', function () {
...@@ -56,24 +57,6 @@ ...@@ -56,24 +57,6 @@
}); });
*/ */
}); });
it('add ARIA attributes to button, menu, and menu items links',
function () {
expect(button).toHaveAttrs({
'role': 'button',
'title': '.srt',
'aria-disabled': 'false'
});
expect(menuList).toHaveAttr('role', 'menu');
menuItemsLinks.each(function(){
expect($(this)).toHaveAttrs({
'role': 'menuitem',
'aria-disabled': 'false'
});
});
});
}); });
describe('when running', function () { describe('when running', function () {
......
(function (WAIT_TIMEOUT) {
'use strict';
describe('VideoBumper', function () {
var state, oldOTBD, waitForPlaying;
waitForPlaying = function (state) {
waitsFor(function () {
return state.el.hasClass('is-playing');
}, 'Player is not playing.', WAIT_TIMEOUT);
};
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice').andReturn(null);
state = jasmine.initializePlayer('video_with_bumper.html');
$('.poster .btn-play').click();
jasmine.Clock.useMock();
});
afterEach(function () {
$('source').remove();
state.storage.clear();
if (state.bumperState && state.bumperState.videoPlayer) {
state.bumperState.videoPlayer.destroy();
}
if (state.videoPlayer) {
state.videoPlayer.destroy();
}
window.onTouchBasedDevice = oldOTBD;
});
it('can render the bumper video', function () {
expect($('.is-bumper')).toExist();
});
it('can show the main video on error', function () {
state.el.trigger('error');
jasmine.Clock.tick(20);
expect($('.is-bumper')).not.toExist();
waitForPlaying(state);
});
it('can show the main video once bumper ends', function () {
state.el.trigger('ended');
jasmine.Clock.tick(20);
expect($('.is-bumper')).not.toExist();
waitForPlaying(state);
});
it('can show the main video on skip', function () {
state.bumperState.videoBumper.skip();
jasmine.Clock.tick(20);
expect($('.is-bumper')).not.toExist();
waitForPlaying(state);
});
it('can stop the bumper video playing if it is too long', function () {
state.el.trigger('timeupdate', [state.bumperState.videoBumper.maxBumperDuration + 1]);
jasmine.Clock.tick(20);
expect($('.is-bumper')).not.toExist();
waitForPlaying(state);
});
it('can save appropriate states correctly on ended', function () {
var saveState = jasmine.createSpy('saveState');
state.bumperState.videoSaveStatePlugin.saveState = saveState;
state.el.trigger('ended');
jasmine.Clock.tick(20);
expect(saveState).toHaveBeenCalledWith(true, {
bumper_last_view_date: true});
});
it('can save appropriate states correctly on skip', function () {
var saveState = jasmine.createSpy('saveState');
state.bumperState.videoSaveStatePlugin.saveState = saveState;
state.bumperState.videoBumper.skip();
expect(state.storage.getItem('isBumperShown')).toBeTruthy();
jasmine.Clock.tick(20);
expect(saveState).toHaveBeenCalledWith(true, {
bumper_last_view_date: true});
});
it('can save appropriate states correctly on error', function () {
var saveState = jasmine.createSpy('saveState');
state.bumperState.videoSaveStatePlugin.saveState = saveState;
state.el.trigger('error');
expect(state.storage.getItem('isBumperShown')).toBeTruthy();
jasmine.Clock.tick(20);
expect(saveState).toHaveBeenCalledWith(true, {
bumper_last_view_date: true});
});
it('can save appropriate states correctly on skip and do not show again', function () {
var saveState = jasmine.createSpy('saveState');
state.bumperState.videoSaveStatePlugin.saveState = saveState;
state.bumperState.videoBumper.skipAndDoNotShowAgain();
expect(state.storage.getItem('isBumperShown')).toBeTruthy();
jasmine.Clock.tick(20);
expect(saveState).toHaveBeenCalledWith(true, {
bumper_last_view_date: true, bumper_do_not_show_again: true});
});
it('can destroy itself', function () {
state.bumperState.videoBumper.destroy();
expect(state.videoBumper).toBeUndefined();
});
});
}).call(this, window.WAIT_TIMEOUT);
...@@ -11,14 +11,13 @@ ...@@ -11,14 +11,13 @@
}); });
afterEach(function () { afterEach(function () {
$('.subtitles').remove();
// `source` tags should be removed to avoid memory leak bug that we // `source` tags should be removed to avoid memory leak bug that we
// had before. Removing of `source` tag, not `video` tag, stops // had before. Removing of `source` tag, not `video` tag, stops
// loading video source and clears the memory. // loading video source and clears the memory.
$('source').remove(); $('source').remove();
$.fn.scrollTo.reset(); $.fn.scrollTo.reset();
state.storage.clear(); state.storage.clear();
state.videoPlayer.destroy();
window.onTouchBasedDevice = oldOTBD; window.onTouchBasedDevice = oldOTBD;
}); });
...@@ -121,11 +120,6 @@ ...@@ -121,11 +120,6 @@
}); });
}); });
it('bind the hide caption button', function () {
state = jasmine.initializePlayer();
expect($('.hide-subtitles')).toHandle('click');
});
it('bind the mouse movement', function () { it('bind the mouse movement', function () {
state = jasmine.initializePlayer(); state = jasmine.initializePlayer();
expect($('.subtitles')).toHandle('mouseover'); expect($('.subtitles')).toHandle('mouseover');
...@@ -143,6 +137,27 @@ ...@@ -143,6 +137,27 @@
}); });
it('can destroy itself', function () {
spyOn($, 'ajaxWithPrefix');
state = jasmine.initializePlayer();
var plugin = state.videoCaption;
spyOn($.fn, 'off').andCallThrough();
state.videoCaption.destroy();
expect(state.videoCaption).toBeUndefined();
expect($.fn.off).toHaveBeenCalledWith({
'caption:fetch': plugin.fetchCaption,
'caption:resize': plugin.onResize,
'caption:update': plugin.onCaptionUpdate,
'ended': plugin.pause,
'fullscreen': plugin.onResize,
'pause': plugin.pause,
'play': plugin.play,
'destroy': plugin.destroy
});
});
describe('renderLanguageMenu', function () { describe('renderLanguageMenu', function () {
describe('is rendered', function () { describe('is rendered', function () {
it('if languages more than 1', function () { it('if languages more than 1', function () {
...@@ -593,7 +608,7 @@ ...@@ -593,7 +608,7 @@
it(msg, function () { it(msg, function () {
spyOn(Caption, 'fetchAvailableTranslations'); spyOn(Caption, 'fetchAvailableTranslations');
$.ajax.andCallFake(function (settings) { $.ajax.andCallFake(function (settings) {
settings.error([]); _.result(settings, 'error');
}); });
state.config.transcriptLanguages = {}; state.config.transcriptLanguages = {};
...@@ -612,7 +627,7 @@ ...@@ -612,7 +627,7 @@
xit(msg, function () { xit(msg, function () {
$.ajax $.ajax
.andCallFake(function (settings) { .andCallFake(function (settings) {
settings.error([]); _.result(settings, 'error');
}); });
state.config.transcriptLanguages = { state.config.transcriptLanguages = {
...@@ -690,7 +705,7 @@ ...@@ -690,7 +705,7 @@
msg = 'on error: captions are hidden if there are no transcript'; msg = 'on error: captions are hidden if there are no transcript';
it(msg, function () { it(msg, function () {
$.ajax.andCallFake(function (settings) { $.ajax.andCallFake(function (settings) {
settings.error(); _.result(settings, 'error');
}); });
Caption.fetchAvailableTranslations(); Caption.fetchAvailableTranslations();
...@@ -907,8 +922,8 @@ ...@@ -907,8 +922,8 @@
$('.subtitles').css('maxHeight'), 10 $('.subtitles').css('maxHeight'), 10
); );
videoWrapperHeight = $('.video-wrapper').height(); videoWrapperHeight = $('.video-wrapper').height();
progressSliderHeight = videoControl.sliderEl.height(); progressSliderHeight = state.el.find('.slider').height();
controlHeight = videoControl.el.height(); controlHeight = state.el.find('.video-controls').height();
shouldBeHeight = videoWrapperHeight - shouldBeHeight = videoWrapperHeight -
0.5 * progressSliderHeight - 0.5 * progressSliderHeight -
controlHeight; controlHeight;
...@@ -1043,7 +1058,6 @@ ...@@ -1043,7 +1058,6 @@
describe('toggle', function () { describe('toggle', function () {
beforeEach(function () { beforeEach(function () {
state = jasmine.initializePlayer(); state = jasmine.initializePlayer();
spyOn(state.videoPlayer, 'log');
$('.subtitles li[data-index=1]').addClass('current'); $('.subtitles li[data-index=1]').addClass('current');
}); });
...@@ -1053,15 +1067,6 @@ ...@@ -1053,15 +1067,6 @@
state.videoCaption.toggle(jQuery.Event('click')); state.videoCaption.toggle(jQuery.Event('click'));
}); });
it('log the hide_transcript event', function () {
expect(state.videoPlayer.log).toHaveBeenCalledWith(
'hide_transcript',
{
currentTime: state.videoPlayer.currentTime
}
);
});
it('hide the caption', function () { it('hide the caption', function () {
expect(state.el).toHaveClass('closed'); expect(state.el).toHaveClass('closed');
}); });
...@@ -1079,15 +1084,6 @@ ...@@ -1079,15 +1084,6 @@
jasmine.Clock.useMock(); jasmine.Clock.useMock();
}); });
it('log the show_transcript event', function () {
expect(state.videoPlayer.log).toHaveBeenCalledWith(
'show_transcript',
{
currentTime: state.videoPlayer.currentTime
}
);
});
it('show the caption', function () { it('show the caption', function () {
expect(state.el).not.toHaveClass('closed'); expect(state.el).not.toHaveClass('closed');
}); });
......
...@@ -68,6 +68,7 @@ ...@@ -68,6 +68,7 @@
$('source').remove(); $('source').remove();
_.result(state.storage, 'clear'); _.result(state.storage, 'clear');
_.result($('video').data('contextmenu'), 'destroy'); _.result($('video').data('contextmenu'), 'destroy');
_.result(state.videoPlayer, 'destroy');
}); });
describe('constructor', function () { describe('constructor', function () {
...@@ -219,12 +220,13 @@ ...@@ -219,12 +220,13 @@
it('mouse left/right-clicking behaves as expected on play/pause menu item', function () { it('mouse left/right-clicking behaves as expected on play/pause menu item', function () {
var menuItem = menuItems.first(); var menuItem = menuItems.first();
spyOn(state.videoPlayer, 'isPlaying');
spyOn(state.videoPlayer, 'play').andCallFake(function () { spyOn(state.videoPlayer, 'play').andCallFake(function () {
state.videoControl.isPlaying = true; state.videoPlayer.isPlaying.andReturn(true);
state.el.trigger('play'); state.el.trigger('play');
}); });
spyOn(state.videoPlayer, 'pause').andCallFake(function () { spyOn(state.videoPlayer, 'pause').andCallFake(function () {
state.videoControl.isPlaying = false; state.videoPlayer.isPlaying.andReturn(false);
state.el.trigger('pause'); state.el.trigger('pause');
}); });
// Left-click on play // Left-click on play
......
(function (undefined) {
'use strict';
describe('VideoPlayer Events Bumper plugin', function () {
var state, oldOTBD;
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice')
.andReturn(null);
jasmine.stubRequests();
state = jasmine.initializePlayer('video_with_bumper.html');
spyOn(Logger, 'log');
$('.poster .btn-play').click();
spyOn(state.bumperState.videoEventsBumperPlugin, 'getCurrentTime').andReturn(10);
spyOn(state.bumperState.videoEventsBumperPlugin, 'getDuration').andReturn(20);
});
afterEach(function () {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
state.storage.clear();
if (state.bumperState && state.bumperState.videoPlayer) {
state.bumperState.videoPlayer.destroy();
}
if (state.videoPlayer) {
state.videoPlayer.destroy();
}
});
it('can emit "edx.video.bumper.loaded" event', function () {
state.el.trigger('ready');
expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.loaded', {
host_component_id: 'id',
bumper_id: 'xmodule/include/fixtures/test.mp4',
code: 'html5',
duration: 20
});
});
it('can emit "edx.video.bumper.played" event', function () {
state.el.trigger('play');
expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.played', {
host_component_id: 'id',
bumper_id: 'xmodule/include/fixtures/test.mp4',
code: 'html5',
currentTime: 10,
duration: 20
});
});
it('can emit "edx.video.bumper.stopped" event', function () {
state.el.trigger('ended');
expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.stopped', {
host_component_id: 'id',
bumper_id: 'xmodule/include/fixtures/test.mp4',
code: 'html5',
currentTime: 10,
duration: 20
});
Logger.log.reset();
state.el.trigger('stop');
expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.stopped', {
host_component_id: 'id',
bumper_id: 'xmodule/include/fixtures/test.mp4',
code: 'html5',
currentTime: 10,
duration: 20
});
});
it('can emit "edx.video.bumper.skipped" event', function () {
state.el.trigger('skip', [false]);
expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.skipped', {
host_component_id: 'id',
bumper_id: 'xmodule/include/fixtures/test.mp4',
code: 'html5',
currentTime: 10,
duration: 20
});
});
it('can emit "edx.video.bumper.dismissed" event', function () {
state.el.trigger('skip', [true]);
expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.dismissed', {
host_component_id: 'id',
bumper_id: 'xmodule/include/fixtures/test.mp4',
code: 'html5',
currentTime: 10,
duration: 20
});
});
it('can emit "edx.video.bumper.transcript.menu.shown" event', function () {
state.el.trigger('language_menu:show');
expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.menu.shown', {
host_component_id: 'id',
bumper_id: 'xmodule/include/fixtures/test.mp4',
code: 'html5',
duration: 20
});
});
it('can emit "edx.video.bumper.transcript.menu.hidden" event', function () {
state.el.trigger('language_menu:hide');
expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.menu.hidden', {
host_component_id: 'id',
bumper_id: 'xmodule/include/fixtures/test.mp4',
code: 'html5',
duration: 20
});
});
it('can emit "edx.video.bumper.transcript.shown" event', function () {
state.el.trigger('captions:show');
expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.shown', {
host_component_id: 'id',
bumper_id: 'xmodule/include/fixtures/test.mp4',
code: 'html5',
currentTime: 10,
duration: 20
});
});
it('can emit "edx.video.bumper.transcript.hidden" event', function () {
state.el.trigger('captions:hide');
expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.hidden', {
host_component_id: 'id',
bumper_id: 'xmodule/include/fixtures/test.mp4',
code: 'html5',
currentTime: 10,
duration: 20
});
});
it('can destroy itself', function () {
var plugin = state.bumperState.videoEventsBumperPlugin;
spyOn($.fn, 'off').andCallThrough();
plugin.destroy();
expect(state.bumperState.videoEventsBumperPlugin).toBeUndefined();
expect($.fn.off).toHaveBeenCalledWith({
'ready': plugin.onReady,
'play': plugin.onPlay,
'ended stop': plugin.onEnded,
'skip': plugin.onSkip,
'language_menu:show': plugin.onShowLanguageMenu,
'language_menu:hide': plugin.onHideLanguageMenu,
'captions:show': plugin.onShowCaptions,
'captions:hide': plugin.onHideCaptions,
'destroy': plugin.destroy
});
});
});
}).call(this);
(function (undefined) {
'use strict';
describe('VideoPlayer Events plugin', function () {
var state, oldOTBD;
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice')
.andReturn(null);
jasmine.stubRequests();
state = jasmine.initializePlayer();
spyOn(Logger, 'log');
spyOn(state.videoEventsPlugin, 'getCurrentTime').andReturn(10);
});
afterEach(function () {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
state.storage.clear();
if (state.videoPlayer) {
state.videoPlayer.destroy();
}
});
it('can emit "load_video" event', function () {
state.el.trigger('ready');
expect(Logger.log).toHaveBeenCalledWith('load_video', {
id: 'id',
code: 'html5'
});
});
it('can emit "play_video" event', function () {
state.el.trigger('play');
expect(Logger.log).toHaveBeenCalledWith('play_video', {
id: 'id',
code: 'html5',
currentTime: 10
});
});
it('can emit "pause_video" event', function () {
state.el.trigger('pause');
expect(Logger.log).toHaveBeenCalledWith('pause_video', {
id: 'id',
code: 'html5',
currentTime: 10
});
});
it('can emit "speed_change_video" event', function () {
state.el.trigger('speedchange', ['2.0', '1.0']);
expect(Logger.log).toHaveBeenCalledWith('speed_change_video', {
id: 'id',
code: 'html5',
current_time: 10,
old_speed: '1.0',
new_speed: '2.0'
});
});
it('can emit "seek_video" event', function () {
state.el.trigger('seek', [1, 0, 'any']);
expect(Logger.log).toHaveBeenCalledWith('seek_video', {
id: 'id',
code: 'html5',
old_time: 0,
new_time: 1,
type: 'any'
});
});
it('can emit "stop_video" event', function () {
state.el.trigger('ended');
expect(Logger.log).toHaveBeenCalledWith('stop_video', {
id: 'id',
code: 'html5',
currentTime: 10
});
Logger.log.reset();
state.el.trigger('stop');
expect(Logger.log).toHaveBeenCalledWith('stop_video', {
id: 'id',
code: 'html5',
currentTime: 10
});
});
it('can emit "skip_video" event', function () {
state.el.trigger('skip', [false]);
expect(Logger.log).toHaveBeenCalledWith('skip_video', {
id: 'id',
code: 'html5',
currentTime: 10
});
});
it('can emit "do_not_show_again_video" event', function () {
state.el.trigger('skip', [true]);
expect(Logger.log).toHaveBeenCalledWith('do_not_show_again_video', {
id: 'id',
code: 'html5',
currentTime: 10
});
});
it('can emit "video_show_cc_menu" event', function () {
state.el.trigger('language_menu:show');
expect(Logger.log).toHaveBeenCalledWith('video_show_cc_menu', {
id: 'id',
code: 'html5'
});
});
it('can emit "video_hide_cc_menu" event', function () {
state.el.trigger('language_menu:hide');
expect(Logger.log).toHaveBeenCalledWith('video_hide_cc_menu', {
id: 'id',
code: 'html5'
});
});
it('can emit "show_transcript" event', function () {
state.el.trigger('captions:show');
expect(Logger.log).toHaveBeenCalledWith('show_transcript', {
id: 'id',
code: 'html5',
current_time: 10
});
});
it('can emit "hide_transcript" event', function () {
state.el.trigger('captions:hide');
expect(Logger.log).toHaveBeenCalledWith('hide_transcript', {
id: 'id',
code: 'html5',
current_time: 10
});
});
it('can destroy itself', function () {
var plugin = state.videoEventsPlugin;
spyOn($.fn, 'off').andCallThrough();
state.videoEventsPlugin.destroy();
expect(state.videoEventsPlugin).toBeUndefined();
expect($.fn.off).toHaveBeenCalledWith({
'ready': plugin.onReady,
'play': plugin.onPlay,
'pause': plugin.onPause,
'ended stop': plugin.onEnded,
'seek': plugin.onSeek,
'skip': plugin.onSkip,
'speedchange': plugin.onSpeedChange,
'language_menu:show': plugin.onShowLanguageMenu,
'language_menu:hide': plugin.onHideLanguageMenu,
'captions:show': plugin.onShowCaptions,
'captions:hide': plugin.onHideCaptions,
'destroy': plugin.destroy
});
});
});
}).call(this);
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
afterEach(function () { afterEach(function () {
// Turn jQuery animations back on. // Turn jQuery animations back on.
jQuery.fx.off = true; jQuery.fx.off = true;
state.videoPlayer.destroy();
}); });
it( it(
......
(function () {
'use strict';
describe('VideoFullScreen', function () {
var state, oldOTBD;
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice').andReturn(null);
});
afterEach(function () {
$('source').remove();
state.storage.clear();
state.videoPlayer.destroy();
window.onTouchBasedDevice = oldOTBD;
});
describe('constructor', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
});
it('renders the fullscreen control', function () {
expect($('.add-fullscreen')).toExist();
expect(state.videoFullScreen.fullScreenState).toBe(false);
});
it('correctly adds ARIA attributes to fullscreen control', function () {
var fullScreenControl = $('.add-fullscreen');
expect(fullScreenControl).toHaveAttrs({
'role': 'button',
'title': 'Fill browser',
'aria-disabled': 'false'
});
});
it('correctly triggers the event handler to toggle fullscreen mode', function () {
spyOn(state.videoFullScreen, 'exit');
spyOn(state.videoFullScreen, 'enter');
state.videoFullScreen.fullScreenState = false;
state.videoFullScreen.toggle();
expect(state.videoFullScreen.enter).toHaveBeenCalled();
state.videoFullScreen.fullScreenState = true;
state.videoFullScreen.toggle();
expect(state.videoFullScreen.exit).toHaveBeenCalled();
});
it('correctly updates ARIA on state change', function () {
var fullScreenControl = $('.add-fullscreen');
fullScreenControl.click();
expect(fullScreenControl).toHaveAttrs({
'role': 'button',
'title': 'Exit full browser',
'aria-disabled': 'false'
});
fullScreenControl.click();
expect(fullScreenControl).toHaveAttrs({
'role': 'button',
'title': 'Fill browser',
'aria-disabled': 'false'
});
});
it('correctly can out of fullscreen by pressing esc', function () {
spyOn(state.videoCommands, 'execute');
var esc = $.Event('keyup');
esc.keyCode = 27;
state.isFullScreen = true;
$(document).trigger(esc);
expect(state.videoCommands.execute).toHaveBeenCalledWith('toggleFullScreen');
});
it('can update video dimensions on state change', function () {
state.el.trigger('fullscreen', [true]);
expect(state.resizer.setMode).toHaveBeenCalledWith('both');
state.el.trigger('fullscreen', [false]);
expect(state.resizer.setMode).toHaveBeenCalledWith('width');
});
it('can destroy itself', function () {
state.videoFullScreen.destroy();
expect($('.add-fullscreen')).not.toExist();
expect(state.videoFullScreen).toBeUndefined();
});
});
it('Controls height is actual on switch to fullscreen', function () {
spyOn($.fn, 'height').andCallFake(function (val) {
return _.isUndefined(val) ? 100: this;
});
state = jasmine.initializePlayer();
$(state.el).trigger('fullscreen');
expect(state.videoFullScreen.height).toBe(150);
});
});
}).call(this);
(function () {
'use strict';
describe('VideoPlayPauseControl', function () {
var state, oldOTBD;
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice').andReturn(null);
state = jasmine.initializePlayer();
spyOn(state.videoCommands, 'execute');
spyOn(state.videoSaveStatePlugin, 'saveState');
});
afterEach(function () {
$('source').remove();
state.storage.clear();
state.videoPlayer.destroy();
window.onTouchBasedDevice = oldOTBD;
});
it('can render the control', function () {
expect($('.video_control.play')).toExist();
});
it('add ARIA attributes to play control', function () {
expect($('.video_control.play')).toHaveAttrs({
'role': 'button',
'title': 'Play',
'aria-disabled': 'false'
});
});
it('can update ARIA state on play', function () {
state.el.trigger('play');
expect($('.video_control.pause')).toHaveAttrs({
'role': 'button',
'title': 'Pause',
'aria-disabled': 'false'
});
});
it('can update ARIA state on video ends', function () {
state.el.trigger('play');
state.el.trigger('ended');
expect($('.video_control.play')).toHaveAttrs({
'role': 'button',
'title': 'Play',
'aria-disabled': 'false'
});
});
it('can update state on pause', function () {
state.el.trigger('pause');
expect(state.videoSaveStatePlugin.saveState).toHaveBeenCalledWith(true);
});
it('can start video playing on click', function () {
$('.video_control.play').click();
expect(state.videoCommands.execute).toHaveBeenCalledWith('togglePlayback');
});
it('can destroy itself', function () {
state.videoPlayPauseControl.destroy();
expect(state.videoPlayPauseControl).toBeUndefined();
});
});
}).call(this);
(function () {
'use strict';
describe('VideoPlayPlaceholder', function () {
var state, oldOTBD;
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice').andReturn(['iPad']);
state = jasmine.initializePlayer();
spyOn(state.videoCommands, 'execute');
});
afterEach(function () {
$('source').remove();
state.storage.clear();
state.videoPlayer.destroy();
window.onTouchBasedDevice = oldOTBD;
});
var cases = [
{
name: 'PC',
isShown: false,
isTouch: null
}, {
name: 'iPad',
isShown: true,
isTouch: ['iPad']
}, {
name: 'Android',
isShown: true,
isTouch: ['Android']
}, {
name: 'iPhone',
isShown: false,
isTouch: ['iPhone']
}
];
beforeEach(function () {
jasmine.stubRequests();
spyOn(window.YT, 'Player').andCallThrough();
});
it ('works correctly on calling proper methods', function () {
var btnPlay;
state = jasmine.initializePlayer();
btnPlay = state.el.find('.btn-play');
state.videoPlayPlaceholder.show();
expect(btnPlay).not.toHaveClass('is-hidden');
expect(btnPlay).toHaveAttrs({
'aria-hidden': 'false',
'tabindex': 0
});
state.videoPlayPlaceholder.hide();
expect(btnPlay).toHaveClass('is-hidden');
expect(btnPlay).toHaveAttrs({
'aria-hidden': 'true',
'tabindex': -1
});
});
$.each(cases, function (index, data) {
var message = [
(data.isShown) ? 'is' : 'is not',
' shown on',
data.name
].join('');
it(message, function () {
var btnPlay;
window.onTouchBasedDevice.andReturn(data.isTouch);
state = jasmine.initializePlayer();
btnPlay = state.el.find('.btn-play');
if (data.isShown) {
expect(btnPlay).not.toHaveClass('is-hidden');
} else {
expect(btnPlay).toHaveClass('is-hidden');
}
});
});
$.each(['iPad', 'Android'], function (index, device) {
it(
'is shown on paused video on ' + device +
' in HTML5 player',
function ()
{
var btnPlay;
window.onTouchBasedDevice.andReturn([device]);
state = jasmine.initializePlayer();
btnPlay = state.el.find('.btn-play');
state.el.trigger('play');
state.el.trigger('pause');
expect(btnPlay).not.toHaveClass('is-hidden');
});
it(
'is hidden on playing video on ' + device +
' in HTML5 player',
function ()
{
var btnPlay;
window.onTouchBasedDevice.andReturn([device]);
state = jasmine.initializePlayer();
btnPlay = state.el.find('.btn-play');
state.el.trigger('play');
expect(btnPlay).toHaveClass('is-hidden');
});
it(
'is hidden on paused video on ' + device +
' in YouTube player',
function ()
{
var btnPlay;
window.onTouchBasedDevice.andReturn([device]);
state = jasmine.initializePlayerYouTube();
btnPlay = state.el.find('.btn-play');
state.el.trigger('play');
state.el.trigger('pause');
expect(btnPlay).toHaveClass('is-hidden');
});
});
it('starts play the video on click', function () {
$('.btn-play').click();
expect(state.videoCommands.execute).toHaveBeenCalledWith('play');
});
it('can destroy itself', function () {
state.videoPlayPlaceholder.destroy();
expect(state.videoPlayPlaceholder).toBeUndefined();
});
});
}).call(this);
(function () {
'use strict';
describe('VideoPlaySkipControl', function () {
var state, oldOTBD;
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice').andReturn(null);
state = jasmine.initializePlayer('video_with_bumper.html');
$('.poster .btn-play').click();
spyOn(state.bumperState.videoCommands, 'execute');
});
afterEach(function () {
$('source').remove();
state.storage.clear();
if (state.bumperState && state.bumperState.videoPlayer) {
state.bumperState.videoPlayer.destroy();
}
window.onTouchBasedDevice = oldOTBD;
});
it('can render the control', function () {
expect($('.video_control.play')).toExist();
});
it('add ARIA attributes to play control', function () {
expect($('.video_control.play')).toHaveAttrs({
'role': 'button',
'title': 'Play',
'aria-disabled': 'false'
});
});
it('can update state on play', function () {
state.el.trigger('play');
expect($('.video_control.play')).not.toExist();
expect($('.video_control.skip')).toExist();
});
it('can start video playing on click', function () {
$('.video_control.play').click();
expect(state.bumperState.videoCommands.execute).toHaveBeenCalledWith('play');
});
it('can skip the video on click', function () {
state.el.trigger('play');
spyOn(state.bumperState.videoPlayer, 'isPlaying').andReturn(true);
$('.video_control.skip').first().click();
expect(state.bumperState.videoCommands.execute).toHaveBeenCalledWith('skip');
});
it('can destroy itself', function () {
var plugin = state.bumperState.videoPlaySkipControl,
el = plugin.el;
spyOn($.fn, 'off').andCallThrough();
plugin.destroy();
expect(state.bumperState.videoPlaySkipControl).toBeUndefined();
expect(el).not.toExist();
expect($.fn.off).toHaveBeenCalledWith('destroy', plugin.destroy);
});
});
}).call(this);
(function (WAIT_TIMEOUT) {
'use strict';
describe('VideoPoster', function () {
var state, oldOTBD;
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice').andReturn(null);
state = jasmine.initializePlayer('video_with_bumper.html');
});
afterEach(function () {
$('source').remove();
state.storage.clear();
if (state.bumperState && state.bumperState.videoPlayer) {
state.bumperState.videoPlayer.destroy();
}
if (state.videoPlayer) {
state.videoPlayer.destroy();
}
window.onTouchBasedDevice = oldOTBD;
});
it('can render the poster', function () {
expect($('.poster')).toExist();
expect($('.btn-play')).toExist();
});
it('can start playing the video on click', function () {
$('.btn-play').click();
waitsFor(function () {
return state.el.hasClass('is-playing');
}, 'Player is not playing.', WAIT_TIMEOUT);
});
it('destroy itself on "play" event', function () {
$('.btn-play').click();
expect($('.poster')).not.toExist();
});
});
}).call(this, window.WAIT_TIMEOUT);
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
$('source').remove(); $('source').remove();
window.onTouchBasedDevice = oldOTBD; window.onTouchBasedDevice = oldOTBD;
state.storage.clear(); state.storage.clear();
state.videoPlayer.destroy();
}); });
describe('constructor', function () { describe('constructor', function () {
...@@ -38,6 +39,18 @@ ...@@ -38,6 +39,18 @@
expect(state.videoProgressSlider.handle) expect(state.videoProgressSlider.handle)
.toBe('.slider .ui-slider-handle'); .toBe('.slider .ui-slider-handle');
}); });
it('add ARIA attributes to time control', function () {
var timeControl = $('div.slider > a');
expect(timeControl).toHaveAttrs({
'role': 'slider',
'title': 'Video position',
'aria-disabled': 'false'
});
expect(timeControl).toHaveAttr('aria-valuetext');
});
}); });
describe('on a touch-based device', function () { describe('on a touch-based device', function () {
...@@ -304,6 +317,13 @@ ...@@ -304,6 +317,13 @@
}); });
}); });
it('can destroy itself', function () {
state = jasmine.initializePlayer();
state.videoProgressSlider.destroy();
expect(state.videoProgressSlider).toBeUndefined();
expect($('.slider')).toBeEmpty();
});
}); });
}).call(this); }).call(this);
(function (undefined) { (function (undefined) {
describe('VideoQualityControl', function () { describe('VideoQualityControl', function () {
var state, qualityControl, qualityControlEl, videoPlayer, player; var state, qualityControl, videoPlayer, player;
afterEach(function () { afterEach(function () {
$('source').remove(); $('source').remove();
if (state.storage) { if (state.storage) {
state.storage.clear(); state.storage.clear();
} }
state.videoPlayer.destroy();
}); });
describe('constructor, YouTube mode', function () { describe('constructor, YouTube mode', function () {
...@@ -105,6 +106,11 @@ ...@@ -105,6 +106,11 @@
expect(qualityControl.el).toHaveClass('active'); expect(qualityControl.el).toHaveClass('active');
}); });
it('can destroy itself', function () {
state.videoQualityControl.destroy();
expect(state.videoQualityControl).toBeUndefined();
expect($('.quality-control')).not.toExist();
});
}); });
describe('constructor, HTML5 mode', function () { describe('constructor, HTML5 mode', function () {
......
(function (undefined) {
'use strict';
describe('VideoPlayer Save State plugin', function () {
var state, oldOTBD;
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice')
.andReturn(null);
jasmine.stubRequests();
state = jasmine.initializePlayer();
spyOn(state.storage, 'setItem');
});
afterEach(function () {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
state.storage.clear();
if (state.videoPlayer) {
state.videoPlayer.destroy();
}
});
describe('saveState function', function () {
var videoPlayerCurrentTime, newCurrentTime, speed;
// We make sure that `currentTime` is a float. We need to test
// that Math.round() is called.
videoPlayerCurrentTime = 3.1242;
// We have two times, because one is stored in
// `videoPlayer.currentTime`, and the other is passed directly to
// `saveState` in `data` object. In each case, there is different
// code that handles these times. They have to be different for
// test completeness sake. Also, make sure it is float, as is the
// time above.
newCurrentTime = 5.4;
speed = '0.75';
beforeEach(function () {
state.videoPlayer.currentTime = videoPlayerCurrentTime;
spyOn(Time, 'formatFull').andCallThrough();
});
it('data is not an object, async is true', function () {
itSpec({
asyncVal: true,
speedVal: undefined,
positionVal: videoPlayerCurrentTime,
data: undefined,
ajaxData: {
saved_video_position: Time.formatFull(Math.round(videoPlayerCurrentTime))
}
});
});
it('data contains speed, async is false', function () {
itSpec({
asyncVal: false,
speedVal: speed,
positionVal: undefined,
data: {
speed: speed
},
ajaxData: {
speed: speed
}
});
});
it('data contains float position, async is true', function () {
itSpec({
asyncVal: true,
speedVal: undefined,
positionVal: newCurrentTime,
data: {
saved_video_position: newCurrentTime
},
ajaxData: {
saved_video_position: Time.formatFull(Math.round(newCurrentTime))
}
});
});
it('data contains speed and rounded position, async is false', function () {
itSpec({
asyncVal: false,
speedVal: speed,
positionVal: Math.round(newCurrentTime),
data: {
speed: speed,
saved_video_position: Math.round(newCurrentTime)
},
ajaxData: {
speed: speed,
saved_video_position: Time.formatFull(Math.round(newCurrentTime))
}
});
});
it('data contains empty object, async is true', function () {
itSpec({
asyncVal: true,
speedVal: undefined,
positionVal: undefined,
data: {},
ajaxData: {}
});
});
it('data contains position 0, async is true', function () {
itSpec({
asyncVal: true,
speedVal: undefined,
positionVal: 0,
data: {
saved_video_position: 0
},
ajaxData: {
saved_video_position: Time.formatFull(Math.round(0))
}
});
});
function itSpec(value) {
var asyncVal = value.asyncVal,
speedVal = value.speedVal,
positionVal = value.positionVal,
data = value.data,
ajaxData = value.ajaxData;
state.videoSaveStatePlugin.saveState(asyncVal, data);
if (speedVal) {
expect(state.storage.setItem).toHaveBeenCalledWith(
'speed',
speedVal,
true
);
}
if (positionVal) {
expect(state.storage.setItem).toHaveBeenCalledWith(
'savedVideoPosition',
positionVal,
true
);
expect(Time.formatFull).toHaveBeenCalledWith(
positionVal
);
}
expect($.ajax).toHaveBeenCalledWith({
url: state.config.saveStateUrl,
type: 'POST',
async: asyncVal,
dataType: 'json',
data: ajaxData
});
}
});
it('can save state on speed change', function () {
state.el.trigger('speedchange', ['2.0']);
expect($.ajax).toHaveBeenCalledWith({
url: state.config.saveStateUrl,
type: 'POST',
async: true,
dataType: 'json',
data: {speed: '2.0'}
});
});
it('can save state on page unload', function () {
$.ajax.reset();
state.videoSaveStatePlugin.onUnload();
expect($.ajax).toHaveBeenCalledWith({
url: state.config.saveStateUrl,
type: 'POST',
async: false,
dataType: 'json',
data: {saved_video_position: '00:00:00'}
});
});
it('can save state on pause', function () {
state.el.trigger('pause');
expect($.ajax).toHaveBeenCalledWith({
url: state.config.saveStateUrl,
type: 'POST',
async: true,
dataType: 'json',
data: {saved_video_position: '00:00:00'}
});
});
it('can save state on language change', function () {
state.el.trigger('language_menu:change', ['ua']);
expect(state.storage.setItem).toHaveBeenCalledWith('language', 'ua');
});
it('can save information about youtube availability', function () {
state.el.trigger('youtube_availability', [true]);
expect($.ajax).toHaveBeenCalledWith({
url: state.config.saveStateUrl,
type: 'POST',
async: true,
dataType: 'json',
data: {youtube_is_available: true}
});
});
it('can destroy itself', function () {
var plugin = state.videoSaveStatePlugin;
spyOn($.fn, 'off').andCallThrough();
state.videoSaveStatePlugin.destroy();
expect(state.videoSaveStatePlugin).toBeUndefined();
expect($.fn.off).toHaveBeenCalledWith({
'speedchange': plugin.onSpeedChange,
'play': plugin.bindUnloadHandler,
'pause destroy': plugin.saveStateHandler,
'language_menu:change': plugin.onLanguageChange,
'youtube_availability': plugin.onYoutubeAvailability
});
expect($.fn.off).toHaveBeenCalledWith('destroy', plugin.destroy);
expect($.fn.off).toHaveBeenCalledWith('unload', plugin.onUnload);
});
});
}).call(this);
(function () {
'use strict';
describe('VideoSkipControl', function () {
var state, oldOTBD;
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice').andReturn(null);
state = jasmine.initializePlayer('video_with_bumper.html');
$('.poster .btn-play').click();
spyOn(state.bumperState.videoCommands, 'execute').andCallThrough();
});
afterEach(function () {
$('source').remove();
state.storage.clear();
if (state.bumperState && state.bumperState.videoPlayer) {
state.bumperState.videoPlayer.destroy();
}
if (state.videoPlayer) {
state.videoPlayer.destroy();
}
window.onTouchBasedDevice = oldOTBD;
});
it('can render the control when video starts playing', function () {
expect($('.skip-control')).not.toExist();
state.el.trigger('play');
expect($('.skip-control')).toExist();
});
it('add ARIA attributes to play control', function () {
state.el.trigger('play');
expect($('.skip-control')).toHaveAttrs({
'role': 'button',
'title': 'Do not show again',
'aria-disabled': 'false'
});
});
it('can skip the video on click', function () {
spyOn(state.bumperState.videoBumper, 'skipAndDoNotShowAgain');
state.el.trigger('play');
$('.skip-control').click();
expect(state.bumperState.videoCommands.execute).toHaveBeenCalledWith('skip', true);
expect(state.bumperState.videoBumper.skipAndDoNotShowAgain).toHaveBeenCalled();
});
it('can destroy itself', function () {
state.bumperState.videoPlaySkipControl.destroy();
expect(state.bumperState.videoPlaySkipControl).toBeUndefined();
});
});
}).call(this);
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
$('source').remove(); $('source').remove();
window.onTouchBasedDevice = oldOTBD; window.onTouchBasedDevice = oldOTBD;
state.storage.clear(); state.storage.clear();
state.videoPlayer.destroy();
}); });
describe('constructor', function () { describe('constructor', function () {
...@@ -247,5 +248,13 @@ ...@@ -247,5 +248,13 @@
expect($('.speeds .value')).toHaveHtml('0.75x'); expect($('.speeds .value')).toHaveHtml('0.75x');
}); });
}); });
it('can destroy itself', function () {
state = jasmine.initializePlayer();
state.videoSpeedControl.destroy();
expect(state.videoSpeedControl).toBeUndefined();
expect($('.video-speeds')).not.toExist();
expect($('.speed-button')).not.toExist();
});
}); });
}).call(this); }).call(this);
...@@ -13,6 +13,7 @@ describe('VideoVolumeControl', function () { ...@@ -13,6 +13,7 @@ describe('VideoVolumeControl', function () {
$('source').remove(); $('source').remove();
window.onTouchBasedDevice = oldOTBD; window.onTouchBasedDevice = oldOTBD;
state.storage.clear(); state.storage.clear();
state.videoPlayer.destroy();
}); });
it('Volume level has correct value even if cookie is broken', function () { it('Volume level has correct value even if cookie is broken', function () {
...@@ -35,8 +36,7 @@ describe('VideoVolumeControl', function () { ...@@ -35,8 +36,7 @@ describe('VideoVolumeControl', function () {
}); });
it('render the volume control', function () { it('render the volume control', function () {
expect(state.videoControl.secondaryControlsEl.html()) expect($('.volume')).toExist();
.toContain('<div class="volume">\n');
}); });
it('create the slider', function () { it('create the slider', function () {
...@@ -292,7 +292,7 @@ describe('VideoVolumeControl', function () { ...@@ -292,7 +292,7 @@ describe('VideoVolumeControl', function () {
shiftKey: true shiftKey: true
}); });
}); });
}) });
describe('keyDownButtonHandler', function () { describe('keyDownButtonHandler', function () {
beforeEach(function () { beforeEach(function () {
...@@ -308,6 +308,6 @@ describe('VideoVolumeControl', function () { ...@@ -308,6 +308,6 @@ describe('VideoVolumeControl', function () {
})); }));
expect(volumeControl.getMuteStatus()).toEqual(isMuted); expect(volumeControl.getMuteStatus()).toEqual(isMuted);
}); });
}) });
}); });
}).call(this); }).call(this);
...@@ -177,9 +177,8 @@ function () { ...@@ -177,9 +177,8 @@ function () {
} }
}; };
var cleanDelta = function () { var resetDelta = function () {
delta['height'] = 0; delta['height'] = delta['width'] = 0;
delta['width'] = 0;
return module; return module;
}; };
...@@ -200,12 +199,23 @@ function () { ...@@ -200,12 +199,23 @@ function () {
return module; return module;
}; };
var destroy = function () {
var data = getData();
data.element.css({
'height': '', 'width': '', 'top': '', 'left': ''
});
removeCallbacks();
resetDelta();
mode = null;
};
initialize.apply(module, arguments); initialize.apply(module, arguments);
return $.extend(true, module, { return $.extend(true, module, {
align: align, align: align,
alignByWidthOnly: alignByWidthOnly, alignByWidthOnly: alignByWidthOnly,
alignByHeightOnly: alignByHeightOnly, alignByHeightOnly: alignByHeightOnly,
destroy: destroy,
setParams: initialize, setParams: initialize,
setMode: setMode, setMode: setMode,
setElement: setElement, setElement: setElement,
...@@ -218,7 +228,7 @@ function () { ...@@ -218,7 +228,7 @@ function () {
delta: { delta: {
add: addDelta, add: addDelta,
substract: substractDelta, substract: substractDelta,
reset: cleanDelta reset: resetDelta
} }
}); });
}; };
......
...@@ -110,6 +110,54 @@ function () { ...@@ -110,6 +110,54 @@ function () {
}); });
}; };
Player.prototype.onError = function (event) {
if ($.isFunction(this.config.events.onError)) {
this.config.events.onError();
}
};
Player.prototype.destroy = function () {
this.video.removeEventListener('loadedmetadata', this.onLoadedMetadata, false);
this.video.removeEventListener('play', this.onPlay, false);
this.video.removeEventListener('playing', this.onPlaying, false);
this.video.removeEventListener('pause', this.onPause, false);
this.video.removeEventListener('ended', this.onEnded, false);
this.el
.find('.video-player div').removeClass('hidden')
.end()
.find('.video-player h3').addClass('hidden')
.end().removeClass('is-initialized')
.find('.spinner').attr({'aria-hidden': 'false'});
this.videoEl.remove();
};
Player.prototype.onLoadedMetadata = function () {
this.playerState = HTML5Video.PlayerState.PAUSED;
if ($.isFunction(this.config.events.onReady)) {
this.config.events.onReady(null);
}
};
Player.prototype.onPlay = function () {
this.playerState = HTML5Video.PlayerState.BUFFERING;
this.callStateChangeCallback();
};
Player.prototype.onPlaying = function () {
this.playerState = HTML5Video.PlayerState.PLAYING;
this.callStateChangeCallback();
};
Player.prototype.onPause = function () {
this.playerState = HTML5Video.PlayerState.PAUSED;
this.callStateChangeCallback();
};
Player.prototype.onEnded = function () {
this.playerState = HTML5Video.PlayerState.ENDED;
this.callStateChangeCallback();
};
return Player; return Player;
/* /*
...@@ -152,6 +200,7 @@ function () { ...@@ -152,6 +200,7 @@ function () {
var isTouch = onTouchBasedDevice() || '', var isTouch = onTouchBasedDevice() || '',
sourceList, _this, errorMessage, lastSource; sourceList, _this, errorMessage, lastSource;
_.bindAll(this, 'onLoadedMetadata', 'onPlay', 'onPlaying', 'onPause', 'onEnded');
this.logs = []; this.logs = [];
// Initially we assume that el is a DOM element. If jQuery selector // Initially we assume that el is a DOM element. If jQuery selector
// fails to select something, we assume that el is an ID of a DOM // fails to select something, we assume that el is an ID of a DOM
...@@ -226,6 +275,8 @@ function () { ...@@ -226,6 +275,8 @@ function () {
lastSource = this.videoEl.find('source').last(); lastSource = this.videoEl.find('source').last();
lastSource.on('error', this.showErrorMessage.bind(this)); lastSource.on('error', this.showErrorMessage.bind(this));
lastSource.on('error', this.onError.bind(this));
this.videoEl.on('error', this.onError.bind(this));
if (/iP(hone|od)/i.test(isTouch[0])) { if (/iP(hone|od)/i.test(isTouch[0])) {
this.videoEl.prop('controls', true); this.videoEl.prop('controls', true);
...@@ -280,35 +331,11 @@ function () { ...@@ -280,35 +331,11 @@ function () {
// When the <video> tag has been processed by the browser, and it // When the <video> tag has been processed by the browser, and it
// is ready for playback, notify other parts of the VideoPlayer, // is ready for playback, notify other parts of the VideoPlayer,
// and initially pause the video. // and initially pause the video.
this.video.addEventListener('loadedmetadata', function () { this.video.addEventListener('loadedmetadata', this.onLoadedMetadata, false);
_this.playerState = HTML5Video.PlayerState.PAUSED; this.video.addEventListener('play', this.onPlay, false);
if ($.isFunction(_this.config.events.onReady)) { this.video.addEventListener('playing', this.onPlaying, false);
_this.config.events.onReady(null); this.video.addEventListener('pause', this.onPause, false);
} this.video.addEventListener('ended', this.onEnded, false);
}, false);
// Register the 'play' event.
this.video.addEventListener('play', function () {
_this.playerState = HTML5Video.PlayerState.BUFFERING;
_this.callStateChangeCallback();
}, false);
this.video.addEventListener('playing', function () {
_this.playerState = HTML5Video.PlayerState.PLAYING;
_this.callStateChangeCallback();
}, false);
// Register the 'pause' event.
this.video.addEventListener('pause', function () {
_this.playerState = HTML5Video.PlayerState.PAUSED;
_this.callStateChangeCallback();
}, false);
// Register the 'ended' event.
this.video.addEventListener('ended', function () {
_this.playerState = HTML5Video.PlayerState.ENDED;
_this.callStateChangeCallback();
}, false);
// Place the <video> element on the page. // Place the <video> element on the page.
this.videoEl.appendTo(this.el.find('.video-player div')); this.videoEl.appendTo(this.el.find('.video-player div'));
......
(function (define) {
'use strict';
define('video/04_video_full_screen.js', [], function () {
var template = [
'<a href="#" class="add-fullscreen" title="',
gettext('Fill browser'), '" role="button" aria-disabled="false">',
gettext('Fill browser'),
'</a>'
].join('');
// VideoControl() function - what this module "exports".
return function (state) {
var dfd = $.Deferred();
state.videoFullScreen = {};
_makeFunctionsPublic(state);
_renderElements(state);
_bindHandlers(state);
dfd.resolve();
return dfd.promise();
};
// ***************************************************************
// Private functions start here.
// ***************************************************************
// function _makeFunctionsPublic(state)
//
// Functions which will be accessible via 'state' object. When called, these functions will
// get the 'state' object as a context.
function _makeFunctionsPublic(state) {
var methodsDict = {
destroy: destroy,
enter: enter,
exitHandler: exitHandler,
exit: exit,
onFullscreenChange: onFullscreenChange,
toggle: toggle,
toggleHandler: toggleHandler,
updateControlsHeight: updateControlsHeight
};
state.bindTo(methodsDict, state.videoFullScreen, state);
}
function destroy() {
$(document).off('keyup', this.videoFullScreen.exitHandler);
this.videoFullScreen.fullScreenEl.remove();
this.el.off({
'fullscreen': this.videoFullScreen.onFullscreenChange,
'destroy': this.videoFullScreen.destroy
});
if (this.isFullScreen) {
this.videoFullScreen.exit();
}
delete this.videoFullScreen;
}
// function _renderElements(state)
//
// Create any necessary DOM elements, attach them, and set their initial configuration. Also
// make the created DOM elements available via the 'state' object. Much easier to work this
// way - you don't have to do repeated jQuery element selects.
function _renderElements(state) {
state.videoFullScreen.fullScreenEl = $(template);
state.videoFullScreen.sliderEl = state.el.find('.slider');
state.videoFullScreen.fullScreenState = false;
state.el.find('.secondary-controls').append(state.videoFullScreen.fullScreenEl);
state.videoFullScreen.updateControlsHeight();
}
// function _bindHandlers(state)
//
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
function _bindHandlers(state) {
state.videoFullScreen.fullScreenEl.on('click', state.videoFullScreen.toggleHandler);
state.el.on({
'fullscreen': state.videoFullScreen.onFullscreenChange,
'destroy': state.videoFullScreen.destroy
});
$(document).on('keyup', state.videoFullScreen.exitHandler);
}
function _getControlsHeight(controls, slider) {
return controls.height() + 0.5 * slider.height();
}
// ***************************************************************
// Public functions start here.
// These are available via the 'state' object. Their context ('this' keyword) is the 'state' object.
// The magic private function that makes them available and sets up their context is makeFunctionsPublic().
// ***************************************************************
function onFullscreenChange (event, isFullScreen) {
var height = this.videoFullScreen.updateControlsHeight();
if (isFullScreen) {
this.resizer
.delta
.substract(height, 'height')
.setMode('both');
} else {
this.resizer
.delta
.reset()
.setMode('width');
}
}
function updateControlsHeight() {
var controls = this.el.find('.video-controls'),
slider = this.videoFullScreen.sliderEl;
this.videoFullScreen.height = _getControlsHeight(controls, slider);
return this.videoFullScreen.height;
}
/**
* Event handler to toggle fullscreen mode.
* @param {jquery Event} event
*/
function toggleHandler(event) {
event.preventDefault();
this.videoCommands.execute('toggleFullScreen');
}
function exit() {
var fullScreenClassNameEl = this.el.add(document.documentElement);
this.videoFullScreen.fullScreenState = this.isFullScreen = false;
fullScreenClassNameEl.removeClass('video-fullscreen');
$(window).scrollTop(this.scrollPos);
this.videoFullScreen.fullScreenEl
.attr('title', gettext('Fill browser'))
.text(gettext('Fill browser'));
this.el.trigger('fullscreen', [this.isFullScreen]);
}
function enter() {
var fullScreenClassNameEl = this.el.add(document.documentElement);
this.scrollPos = $(window).scrollTop();
$(window).scrollTop(0);
this.videoFullScreen.fullScreenState = this.isFullScreen = true;
fullScreenClassNameEl.addClass('video-fullscreen');
this.videoFullScreen.fullScreenEl
.attr('title', gettext('Exit full browser'))
.text(gettext('Exit full browser'));
this.el.trigger('fullscreen', [this.isFullScreen]);
}
/** Toggle fullscreen mode. */
function toggle() {
if (this.videoFullScreen.fullScreenState) {
this.videoFullScreen.exit();
} else {
this.videoFullScreen.enter();
}
}
/**
* Event handler to exit from fullscreen mode.
* @param {jquery Event} event
*/
function exitHandler(event) {
if ((this.isFullScreen) && (event.keyCode === 27)) {
event.preventDefault();
this.videoCommands.execute('toggleFullScreen');
}
}
});
}(RequireJS.define));
...@@ -5,6 +5,12 @@ define( ...@@ -5,6 +5,12 @@ define(
'video/05_video_quality_control.js', 'video/05_video_quality_control.js',
[], [],
function () { function () {
var template = [
'<a href="#" class="quality-control is-hidden" title="',
gettext('HD off'), '" role="button" aria-disabled="false">',
gettext('HD off'),
'</a>'
].join('');
// VideoQualityControl() function - what this module "exports". // VideoQualityControl() function - what this module "exports".
return function (state) { return function (state) {
...@@ -12,7 +18,6 @@ function () { ...@@ -12,7 +18,6 @@ function () {
// Changing quality for now only works for YouTube videos. // Changing quality for now only works for YouTube videos.
if (state.videoType !== 'youtube') { if (state.videoType !== 'youtube') {
state.el.find('a.quality-control').remove();
return; return;
} }
...@@ -36,6 +41,7 @@ function () { ...@@ -36,6 +41,7 @@ function () {
// get the 'state' object as a context. // get the 'state' object as a context.
function _makeFunctionsPublic(state) { function _makeFunctionsPublic(state) {
var methodsDict = { var methodsDict = {
destroy: destroy,
fetchAvailableQualities: fetchAvailableQualities, fetchAvailableQualities: fetchAvailableQualities,
onQualityChange: onQualityChange, onQualityChange: onQualityChange,
showQualityControl: showQualityControl, showQualityControl: showQualityControl,
...@@ -45,16 +51,25 @@ function () { ...@@ -45,16 +51,25 @@ function () {
state.bindTo(methodsDict, state.videoQualityControl, state); state.bindTo(methodsDict, state.videoQualityControl, state);
} }
function destroy() {
this.videoQualityControl.el.off({
'click': this.videoQualityControl.toggleQuality,
'destroy': this.videoQualityControl.destroy
});
this.el.off('.quality');
this.videoQualityControl.el.remove();
delete this.videoQualityControl;
}
// function _renderElements(state) // function _renderElements(state)
// //
// Create any necessary DOM elements, attach them, and set their initial configuration. Also // Create any necessary DOM elements, attach them, and set their initial configuration. Also
// make the created DOM elements available via the 'state' object. Much easier to work this // make the created DOM elements available via the 'state' object. Much easier to work this
// way - you don't have to do repeated jQuery element selects. // way - you don't have to do repeated jQuery element selects.
function _renderElements(state) { function _renderElements(state) {
state.videoQualityControl.el = state.el.find('a.quality-control'); var element = state.videoQualityControl.el = $(template);
state.videoQualityControl.el.show();
state.videoQualityControl.quality = 'large'; state.videoQualityControl.quality = 'large';
state.el.find('.secondary-controls').append(element);
} }
// function _bindHandlers(state) // function _bindHandlers(state)
...@@ -64,9 +79,11 @@ function () { ...@@ -64,9 +79,11 @@ function () {
state.videoQualityControl.el.on('click', state.videoQualityControl.el.on('click',
state.videoQualityControl.toggleQuality state.videoQualityControl.toggleQuality
); );
state.el.on('play', _.once( state.el.on('play.quality', _.once(
state.videoQualityControl.fetchAvailableQualities state.videoQualityControl.fetchAvailableQualities
)); ));
state.el.on('destroy.quality', state.videoQualityControl.destroy);
} }
// *************************************************************** // ***************************************************************
...@@ -141,7 +158,7 @@ function () { ...@@ -141,7 +158,7 @@ function () {
event.preventDefault(); event.preventDefault();
newQuality = isHD ? 'large' : 'highres'; newQuality = isHD ? 'large' : 'highres';
this.trigger('videoPlayer.handlePlaybackQualityChange', newQuality); this.trigger('videoPlayer.handlePlaybackQualityChange', newQuality);
} }
......
...@@ -12,15 +12,17 @@ define( ...@@ -12,15 +12,17 @@ define(
'video/06_video_progress_slider.js', 'video/06_video_progress_slider.js',
[], [],
function () { function () {
var template = [
'<div class="slider" title="', gettext('Video position'), '"></div>'
].join('');
// VideoProgressSlider() function - what this module "exports". // VideoProgressSlider() function - what this module "exports".
return function (state) { return function (state) {
var dfd = $.Deferred(); var dfd = $.Deferred();
state.videoProgressSlider = {}; state.videoProgressSlider = {};
_makeFunctionsPublic(state); _makeFunctionsPublic(state);
_renderElements(state); _renderElements(state);
// No callbacks to DOM events (click, mousemove, etc.).
dfd.resolve(); dfd.resolve();
return dfd.promise(); return dfd.promise();
...@@ -36,6 +38,7 @@ function () { ...@@ -36,6 +38,7 @@ function () {
// these functions will get the 'state' object as a context. // these functions will get the 'state' object as a context.
function _makeFunctionsPublic(state) { function _makeFunctionsPublic(state) {
var methodsDict = { var methodsDict = {
destroy: destroy,
buildSlider: buildSlider, buildSlider: buildSlider,
getRangeParams: getRangeParams, getRangeParams: getRangeParams,
onSlide: onSlide, onSlide: onSlide,
...@@ -49,6 +52,12 @@ function () { ...@@ -49,6 +52,12 @@ function () {
state.bindTo(methodsDict, state.videoProgressSlider, state); state.bindTo(methodsDict, state.videoProgressSlider, state);
} }
function destroy() {
this.videoProgressSlider.el.removeAttr('tabindex').slider('destroy');
this.el.off('destroy', this.videoProgressSlider.destroy);
delete this.videoProgressSlider;
}
// function _renderElements(state) // function _renderElements(state)
// //
// Create any necessary DOM elements, attach them, and set their // Create any necessary DOM elements, attach them, and set their
...@@ -56,8 +65,9 @@ function () { ...@@ -56,8 +65,9 @@ function () {
// via the 'state' object. Much easier to work this way - you don't // via the 'state' object. Much easier to work this way - you don't
// have to do repeated jQuery element selects. // have to do repeated jQuery element selects.
function _renderElements(state) { function _renderElements(state) {
state.videoProgressSlider.el = state.videoControl.sliderEl; state.videoProgressSlider.el = $(template);
state.el.find('.video-controls').prepend(state.videoProgressSlider.el);
state.videoProgressSlider.buildSlider(); state.videoProgressSlider.buildSlider();
_buildHandle(state); _buildHandle(state);
} }
...@@ -81,6 +91,8 @@ function () { ...@@ -81,6 +91,8 @@ function () {
'aria-valuemin': '0', 'aria-valuemin': '0',
'aria-valuenow': state.videoPlayer.currentTime 'aria-valuenow': state.videoPlayer.currentTime
}); });
state.el.on('destroy', state.videoProgressSlider.destroy);
} }
// *************************************************************** // ***************************************************************
...@@ -109,7 +121,7 @@ function () { ...@@ -109,7 +121,7 @@ function () {
// whole slider). Remember that endTime === null means the end-time // whole slider). Remember that endTime === null means the end-time
// is set to the end of video by default. // is set to the end of video by default.
function updateStartEndTimeRegion(params) { function updateStartEndTimeRegion(params) {
var left, width, start, end, duration, rangeParams; var start, end, duration, rangeParams;
// We must have a duration in order to determine the area of range. // We must have a duration in order to determine the area of range.
// It also must be non-zero. // It also must be non-zero.
......
...@@ -17,6 +17,10 @@ function() { ...@@ -17,6 +17,10 @@ function() {
return new VolumeControl(state, i18n); return new VolumeControl(state, i18n);
} }
_.bindAll(this, 'keyDownHandler', 'updateVolumeSilently',
'onVolumeChangeHandler', 'openMenu', 'closeMenu',
'toggleMuteHandler', 'keyDownButtonHandler', 'destroy'
);
this.state = state; this.state = state;
this.state.videoVolumeControl = this; this.state.videoVolumeControl = this;
this.i18n = i18n; this.i18n = i18n;
...@@ -33,17 +37,55 @@ function() { ...@@ -33,17 +37,55 @@ function() {
/** Step to increase/decrease volume level via keyboard. */ /** Step to increase/decrease volume level via keyboard. */
step: 20, step: 20,
template: [
'<div class="volume">',
'<a href="#" role="button" aria-disabled="false" title="',
gettext('Volume'), '" aria-label="',
gettext('Click on this button to mute or unmute this video or press UP or DOWN buttons to increase or decrease volume level.'),
'"></a>',
'<div role="presentation" class="volume-slider-container">',
'<div class="volume-slider"></div>',
'</div>',
'</div>'
].join(''),
destroy: function () {
this.volumeSlider.slider('destroy');
this.state.el.find('iframe').removeAttr('tabindex');
this.a11y.destroy();
this.cookie = this.a11y = null;
this.closeMenu();
this.state.el
.off('play.volume')
.off({
'keydown': this.keyDownHandler,
'volumechange': this.onVolumeChangeHandler
});
this.el.off({
'mouseenter': this.openMenu,
'mouseleave': this.closeMenu
});
this.button.off({
'mousedown': this.toggleMuteHandler,
'keydown': this.keyDownButtonHandler,
'focus': this.openMenu,
'blur': this.closeMenu
});
this.el.remove();
delete this.state.videoVolumeControl;
},
/** Initializes the module. */ /** Initializes the module. */
initialize: function() { initialize: function() {
var volume; var volume;
this.el = this.state.el.find('.volume');
if (this.state.isTouch) { if (this.state.isTouch) {
// iOS doesn't support volume change // iOS doesn't support volume change
this.el.remove();
return false; return false;
} }
this.el = $(this.template);
// Youtube iframe react on key buttons and has his own handlers. // Youtube iframe react on key buttons and has his own handlers.
// So, we disallow focusing on iframe. // So, we disallow focusing on iframe.
this.state.el.find('iframe').attr('tabindex', -1); this.state.el.find('iframe').attr('tabindex', -1);
...@@ -80,26 +122,28 @@ function() { ...@@ -80,26 +122,28 @@ function() {
// Therefore, we do not need redundant focusing on slider in TAB // Therefore, we do not need redundant focusing on slider in TAB
// order. // order.
container.find('a').attr('tabindex', -1); container.find('a').attr('tabindex', -1);
this.state.el.find('.secondary-controls').append(this.el);
}, },
/** Bind any necessary function callbacks to DOM events. */ /** Bind any necessary function callbacks to DOM events. */
bindHandlers: function() { bindHandlers: function() {
this.state.el.on({ this.state.el.on({
'keydown': this.keyDownHandler.bind(this), 'keydown': this.keyDownHandler,
'play': _.once(this.updateVolumeSilently.bind(this)), 'play.volume': _.once(this.updateVolumeSilently),
'volumechange': this.onVolumeChangeHandler.bind(this) 'volumechange': this.onVolumeChangeHandler
}); });
this.el.on({ this.el.on({
'mouseenter': this.openMenu.bind(this), 'mouseenter': this.openMenu,
'mouseleave': this.closeMenu.bind(this) 'mouseleave': this.closeMenu
}); });
this.button.on({ this.button.on({
'click': false, 'click': false,
'mousedown': this.toggleMuteHandler.bind(this), 'mousedown': this.toggleMuteHandler,
'keydown': this.keyDownButtonHandler.bind(this), 'keydown': this.keyDownButtonHandler,
'focus': this.openMenu.bind(this), 'focus': this.openMenu,
'blur': this.closeMenu.bind(this) 'blur': this.closeMenu
}); });
this.state.el.on('destroy', this.destroy);
}, },
/** /**
...@@ -343,6 +387,10 @@ function() { ...@@ -343,6 +387,10 @@ function() {
}; };
Accessibility.prototype = { Accessibility.prototype = {
destroy: function () {
this.liveRegion.remove();
},
/** Initializes the module. */ /** Initializes the module. */
initialize: function() { initialize: function() {
this.liveRegion = $('<div />', { this.liveRegion = $('<div />', {
......
...@@ -16,6 +16,10 @@ function (Iterator) { ...@@ -16,6 +16,10 @@ function (Iterator) {
return new SpeedControl(state); return new SpeedControl(state);
} }
_.bindAll(this, 'onSetSpeed', 'onRenderSpeed', 'clickLinkHandler',
'keyDownLinkHandler', 'mouseEnterHandler', 'mouseLeaveHandler',
'clickMenuHandler', 'keyDownMenuHandler', 'destroy'
);
this.state = state; this.state = state;
this.state.videoSpeedControl = this; this.state.videoSpeedControl = this;
this.initialize(); this.initialize();
...@@ -24,24 +28,51 @@ function (Iterator) { ...@@ -24,24 +28,51 @@ function (Iterator) {
}; };
SpeedControl.prototype = { SpeedControl.prototype = {
template: [
'<div class="speeds menu-container">',
'<a class="speed-button" href="#" title="',
gettext('Speeds'), '" role="button" aria-disabled="false">',
'<span class="label">', gettext('Speed'), '</span>',
'<span class="value"></span>',
'</a>',
'<ol class="video-speeds menu" role="menu"></ol>',
'</div>'
].join(''),
destroy: function () {
this.el.off({
'mouseenter': this.mouseEnterHandler,
'mouseleave': this.mouseLeaveHandler,
'click': this.clickMenuHandler,
'keydown': this.keyDownMenuHandler
});
this.state.el.off({
'speed:set': this.onSetSpeed,
'speed:render': this.onRenderSpeed
});
this.closeMenu(true);
this.speedsContainer.remove();
this.el.remove();
delete this.state.videoSpeedControl;
},
/** Initializes the module. */ /** Initializes the module. */
initialize: function () { initialize: function () {
var state = this.state; var state = this.state;
this.el = state.el.find('.speeds');
this.speedsContainer = this.el.find('.video-speeds');
this.speedButton = this.el.find('.speed-button');
if (!this.isPlaybackRatesSupported(state)) { if (!this.isPlaybackRatesSupported(state)) {
this.el.remove();
console.log( console.log(
'[Video info]: playbackRate is not supported.' '[Video info]: playbackRate is not supported.'
); );
return false; return false;
} }
this.el = $(this.template);
this.speedsContainer = this.el.find('.video-speeds');
this.speedButton = this.el.find('.speed-button');
this.render(state.speeds, state.speed); this.render(state.speeds, state.speed);
this.setSpeed(state.speed, true, true);
this.bindHandlers(); this.bindHandlers();
return true; return true;
...@@ -51,13 +82,11 @@ function (Iterator) { ...@@ -51,13 +82,11 @@ function (Iterator) {
* Creates any necessary DOM elements, attach them, and set their, * Creates any necessary DOM elements, attach them, and set their,
* initial configuration. * initial configuration.
* @param {array} speeds List of speeds available for the player. * @param {array} speeds List of speeds available for the player.
* @param {string|number} currentSpeed Current speed for the player.
*/ */
render: function (speeds, currentSpeed) { render: function (speeds) {
var self = this, var speedsContainer = this.speedsContainer,
speedsContainer = this.speedsContainer,
reversedSpeeds = speeds.concat().reverse(), reversedSpeeds = speeds.concat().reverse(),
speedsList = $.map(reversedSpeeds, function (speed, index) { speedsList = $.map(reversedSpeeds, function (speed) {
return [ return [
'<li data-speed="', speed, '" role="presentation">', '<li data-speed="', speed, '" role="presentation">',
'<a class="speed-link" href="#" role="menuitem" tabindex="-1">', '<a class="speed-link" href="#" role="menuitem" tabindex="-1">',
...@@ -69,7 +98,7 @@ function (Iterator) { ...@@ -69,7 +98,7 @@ function (Iterator) {
speedsContainer.html(speedsList.join('')); speedsContainer.html(speedsList.join(''));
this.speedLinks = new Iterator(speedsContainer.find('.speed-link')); this.speedLinks = new Iterator(speedsContainer.find('.speed-link'));
this.setSpeed(currentSpeed, true, true); this.state.el.find('.secondary-controls').prepend(this.el);
}, },
/** /**
...@@ -77,31 +106,34 @@ function (Iterator) { ...@@ -77,31 +106,34 @@ function (Iterator) {
* mousemove, etc.). * mousemove, etc.).
*/ */
bindHandlers: function () { bindHandlers: function () {
var self = this;
// Attach various events handlers to the speed menu button. // Attach various events handlers to the speed menu button.
this.el.on({ this.el.on({
'mouseenter': this.mouseEnterHandler.bind(this), 'mouseenter': this.mouseEnterHandler,
'mouseleave': this.mouseLeaveHandler.bind(this), 'mouseleave': this.mouseLeaveHandler,
'click': this.clickMenuHandler.bind(this), 'click': this.clickMenuHandler,
'keydown': this.keyDownMenuHandler.bind(this) 'keydown': this.keyDownMenuHandler
}); });
// Attach click and keydown event handlers to the individual speed // Attach click and keydown event handlers to the individual speed
// entries. // entries.
this.speedsContainer.on({ this.speedsContainer.on({
click: this.clickLinkHandler.bind(this), click: this.clickLinkHandler,
keydown: this.keyDownLinkHandler.bind(this) keydown: this.keyDownLinkHandler
}, 'a.speed-link'); }, 'a.speed-link');
this.state.el.on({ this.state.el.on({
'speed:set': function (event, speed) { 'speed:set': this.onSetSpeed,
self.setSpeed(speed, true); 'speed:render': this.onRenderSpeed
},
'speed:render': function (event, speeds, currentSpeed) {
self.render(speeds, currentSpeed);
}
}); });
this.state.el.on('destroy', this.destroy);
},
onSetSpeed: function (event, speed) {
this.setSpeed(speed, true);
},
onRenderSpeed: function (event, speeds, currentSpeed) {
this.render(speeds, currentSpeed);
}, },
/** /**
...@@ -133,7 +165,7 @@ function (Iterator) { ...@@ -133,7 +165,7 @@ function (Iterator) {
// element to have clicks close the menu when they happen // element to have clicks close the menu when they happen
// outside of it. // outside of it.
if (bindEvent) { if (bindEvent) {
$(window).on('click.speedMenu', this.clickMenuHandler.bind(this)); $(window).on('click.speedMenu', this.clickMenuHandler);
} }
this.el.addClass('is-opened'); this.el.addClass('is-opened');
...@@ -175,7 +207,7 @@ function (Iterator) { ...@@ -175,7 +207,7 @@ function (Iterator) {
this.currentSpeed = speed; this.currentSpeed = speed;
if (!silent) { if (!silent) {
this.el.trigger('speedchange', [speed]); this.el.trigger('speedchange', [speed, this.state.speed]);
} }
} }
}, },
......
...@@ -656,6 +656,12 @@ function (Component) { ...@@ -656,6 +656,12 @@ function (Component) {
if (!state.isYoutubeType()) { if (!state.isYoutubeType()) {
state.el.find('video').contextmenu(state.el, options); state.el.find('video').contextmenu(state.el, options);
state.el.on('destroy', function () {
var contextmenu = $(this).find('video').data('contextmenu');
if (contextmenu) {
contextmenu.destroy();
}
});
} }
return $.Deferred().resolve().promise(); return $.Deferred().resolve().promise();
......
(function (define) {
'use strict';
define('video/09_bumper.js',[], function () {
/**
* VideoBumper module.
* @exports video/09_bumper.js
* @constructor
* @param {Object} player The player factory.
* @param {Object} state The object containing the state of the video
* @return {jquery Promise}
*/
var VideoBumper = function (player, state) {
if (!(this instanceof VideoBumper)) {
return new VideoBumper(player, state);
}
_.bindAll(
this, 'showMainVideoHandler', 'destroy', 'skipByDuration', 'destroyAndResolve'
);
this.dfd = $.Deferred();
this.element = state.el;
this.element.addClass('is-bumper');
this.player = player;
this.state = state;
this.doNotShowAgain = false;
this.state.videoBumper = this;
this.bindHandlers();
this.initialize();
this.maxBumperDuration = 35; // seconds
};
VideoBumper.prototype = {
initialize: function () {
this.player();
},
getPromise: function () {
return this.dfd.promise();
},
showMainVideoHandler: function () {
this.state.storage.setItem('isBumperShown', true);
setTimeout(function () {
this.saveState();
this.showMainVideo();
}.bind(this), 20);
},
destroyAndResolve: function () {
this.destroy();
this.dfd.resolve();
},
showMainVideo: function () {
if (this.state.videoPlayer) {
this.destroyAndResolve();
} else {
this.state.el.on('initialize', this.destroyAndResolve);
}
},
skip: function () {
this.element.trigger('skip', [this.doNotShowAgain]);
this.showMainVideoHandler();
},
skipAndDoNotShowAgain: function () {
this.doNotShowAgain = true;
this.skip();
},
skipByDuration: function (event, time) {
if (time > this.maxBumperDuration) {
this.element.trigger('ended');
}
},
bindHandlers: function () {
var events = ['ended', 'error'].join(' ');
this.element.on(events, this.showMainVideoHandler);
this.element.on('timeupdate', this.skipByDuration);
},
saveState: function () {
var info = {bumper_last_view_date: true};
if (this.doNotShowAgain) {
_.extend(info, {bumper_do_not_show_again: true});
}
this.state.videoSaveStatePlugin.saveState(true, info);
},
destroy: function () {
var events = ['ended', 'error'].join(' ');
this.element.off(events, this.showMainVideoHandler);
this.element.off({
'timeupdate': this.skipByDuration,
'initialize': this.destroyAndResolve
});
this.element.removeClass('is-bumper');
if (_.isFunction(this.state.videoPlayer.destroy)) {
this.state.videoPlayer.destroy();
}
delete this.state.videoBumper;
}
};
return VideoBumper;
});
}(RequireJS.define));
(function(define) {
'use strict';
define('video/09_events_bumper_plugin.js', [], function() {
/**
* Events module.
* @exports video/09_events_bumper_plugin.js
* @constructor
* @param {Object} state The object containing the state of the video
* @param {Object} i18n The object containing strings with translations.
* @param {Object} options
* @return {jquery Promise}
*/
var EventsBumperPlugin = function(state, i18n, options) {
if (!(this instanceof EventsBumperPlugin)) {
return new EventsBumperPlugin(state, i18n, options);
}
_.bindAll(this, 'onReady', 'onPlay', 'onEnded', 'onShowLanguageMenu', 'onHideLanguageMenu', 'onSkip',
'onShowCaptions', 'onHideCaptions', 'destroy');
this.state = state;
this.options = _.extend({}, options);
this.state.videoEventsBumperPlugin = this;
this.i18n = i18n;
this.initialize();
return $.Deferred().resolve().promise();
};
EventsBumperPlugin.moduleName = 'EventsBumperPlugin';
EventsBumperPlugin.prototype = {
destroy: function () {
this.state.el.off(this.events);
delete this.state.videoEventsBumperPlugin;
},
initialize: function() {
this.events = {
'ready': this.onReady,
'play': this.onPlay,
'ended stop': this.onEnded,
'skip': this.onSkip,
'language_menu:show': this.onShowLanguageMenu,
'language_menu:hide': this.onHideLanguageMenu,
'captions:show': this.onShowCaptions,
'captions:hide': this.onHideCaptions,
'destroy': this.destroy
};
this.bindHandlers();
},
bindHandlers: function() {
this.state.el.on(this.events);
},
onReady: function () {
this.log('edx.video.bumper.loaded');
},
onPlay: function () {
this.log('edx.video.bumper.played', {currentTime: this.getCurrentTime()});
},
onEnded: function () {
this.log('edx.video.bumper.stopped', {currentTime: this.getCurrentTime()});
},
onSkip: function (event, doNotShowAgain) {
var info = {currentTime: this.getCurrentTime()},
eventName = 'edx.video.bumper.' + (doNotShowAgain ? 'dismissed': 'skipped');
this.log(eventName, info);
},
onShowLanguageMenu: function () {
this.log('edx.video.bumper.transcript.menu.shown');
},
onHideLanguageMenu: function () {
this.log('edx.video.bumper.transcript.menu.hidden');
},
onShowCaptions: function () {
this.log('edx.video.bumper.transcript.shown', {currentTime: this.getCurrentTime()});
},
onHideCaptions: function () {
this.log('edx.video.bumper.transcript.hidden', {currentTime: this.getCurrentTime()});
},
getCurrentTime: function () {
var player = this.state.videoPlayer;
return player ? player.currentTime : 0;
},
getDuration: function () {
var player = this.state.videoPlayer;
return player ? player.duration() : 0;
},
log: function (eventName, data) {
var logInfo = _.extend({
host_component_id: this.state.id,
bumper_id: this.state.config.sources[0] || '',
duration: this.getDuration(),
code: 'html5'
}, data, this.options.data);
Logger.log(eventName, logInfo);
}
};
return EventsBumperPlugin;
});
}(RequireJS.define));
(function(define) {
'use strict';
define('video/09_events_plugin.js', [], function() {
/**
* Events module.
* @exports video/09_events_plugin.js
* @constructor
* @param {Object} state The object containing the state of the video
* @param {Object} i18n The object containing strings with translations.
* @param {Object} options
* @return {jquery Promise}
*/
var EventsPlugin = function(state, i18n, options) {
if (!(this instanceof EventsPlugin)) {
return new EventsPlugin(state, i18n, options);
}
_.bindAll(this, 'onReady', 'onPlay', 'onPause', 'onEnded', 'onSeek',
'onSpeedChange', 'onShowLanguageMenu', 'onHideLanguageMenu', 'onSkip',
'onShowCaptions', 'onHideCaptions', 'destroy');
this.state = state;
this.options = _.extend({}, options);
this.state.videoEventsPlugin = this;
this.i18n = i18n;
this.initialize();
return $.Deferred().resolve().promise();
};
EventsPlugin.moduleName = 'EventsPlugin';
EventsPlugin.prototype = {
destroy: function () {
this.state.el.off(this.events);
delete this.state.videoEventsPlugin;
},
initialize: function() {
this.events = {
'ready': this.onReady,
'play': this.onPlay,
'pause': this.onPause,
'ended stop': this.onEnded,
'seek': this.onSeek,
'skip': this.onSkip,
'speedchange': this.onSpeedChange,
'language_menu:show': this.onShowLanguageMenu,
'language_menu:hide': this.onHideLanguageMenu,
'captions:show': this.onShowCaptions,
'captions:hide': this.onHideCaptions,
'destroy': this.destroy
};
this.bindHandlers();
},
bindHandlers: function() {
this.state.el.on(this.events);
},
onReady: function () {
this.log('load_video');
},
onPlay: function () {
this.log('play_video', {currentTime: this.getCurrentTime()});
},
onPause: function () {
this.log('pause_video', {currentTime: this.getCurrentTime()});
},
onEnded: function () {
this.log('stop_video', {currentTime: this.getCurrentTime()});
},
onSkip: function (event, doNotShowAgain) {
var info = {currentTime: this.getCurrentTime()},
eventName = doNotShowAgain ? 'do_not_show_again_video': 'skip_video';
this.log(eventName, info);
},
onSeek: function (event, time, oldTime, type) {
this.log('seek_video', {
old_time: oldTime,
new_time: time,
type: type
});
},
onSpeedChange: function (event, newSpeed, oldSpeed) {
this.log('speed_change_video', {
current_time: this.getCurrentTime(),
old_speed: oldSpeed,
new_speed: newSpeed
});
},
onShowLanguageMenu: function () {
this.log('video_show_cc_menu');
},
onHideLanguageMenu: function () {
this.log('video_hide_cc_menu');
},
onShowCaptions: function () {
this.log('show_transcript', {current_time: this.getCurrentTime()});
},
onHideCaptions: function () {
this.log('hide_transcript', {current_time: this.getCurrentTime()});
},
getCurrentTime: function () {
var player = this.state.videoPlayer;
return player ? player.currentTime : 0;
},
log: function (eventName, data) {
var logInfo = _.extend({
id: this.state.id,
code: this.state.isYoutubeType() ? this.state.youtubeId() : 'html5'
}, data, this.options.data);
Logger.log(eventName, logInfo);
}
};
return EventsPlugin;
});
}(RequireJS.define));
(function(define) {
'use strict';
define('video/09_play_pause_control.js', [], function() {
/**
* Play/pause control module.
* @exports video/09_play_pause_control.js
* @constructor
* @param {Object} state The object containing the state of the video
* @param {Object} i18n The object containing strings with translations.
* @return {jquery Promise}
*/
var PlayPauseControl = function(state, i18n) {
if (!(this instanceof PlayPauseControl)) {
return new PlayPauseControl(state, i18n);
}
_.bindAll(this, 'play', 'pause', 'onClick', 'destroy');
this.state = state;
this.state.videoPlayPauseControl = this;
this.i18n = i18n;
this.initialize();
return $.Deferred().resolve().promise();
};
PlayPauseControl.prototype = {
template: [
'<a class="video_control play" href="#" title="',
gettext('Play'), '" role="button" aria-disabled="false">',
gettext('Play'),
'</a>'
].join(''),
destroy: function () {
this.el.remove();
this.state.el.off('destroy', this.destroy);
delete this.state.videoPlayPauseControl;
},
/** Initializes the module. */
initialize: function() {
this.el = $(this.template);
this.render();
this.bindHandlers();
},
/**
* Creates any necessary DOM elements, attach them, and set their,
* initial configuration.
*/
render: function() {
this.state.el.find('.vcr').prepend(this.el);
},
/** Bind any necessary function callbacks to DOM events. */
bindHandlers: function() {
this.el.on({
'click': this.onClick
});
this.state.el.on({
'play': this.play,
'pause ended': this.pause,
'destroy': this.destroy
});
},
onClick: function (event) {
event.preventDefault();
this.state.videoCommands.execute('togglePlayback');
},
play: function () {
this.el
.attr('title', this.i18n['Pause']).text(this.i18n['Pause'])
.removeClass('play').addClass('pause');
},
pause: function () {
this.el
.attr('title', this.i18n['Play']).text(this.i18n['Play'])
.removeClass('pause').addClass('play');
}
};
return PlayPauseControl;
});
}(RequireJS.define));
(function(define) {
'use strict';
define('video/09_play_placeholder.js', [], function() {
/**
* Play placeholder control module.
* @exports video/09_play_placeholder.js
* @constructor
* @param {Object} state The object containing the state of the video
* @param {Object} i18n The object containing strings with translations.
* @return {jquery Promise}
*/
var PlayPlaceholder = function(state, i18n) {
if (!(this instanceof PlayPlaceholder)) {
return new PlayPlaceholder(state, i18n);
}
_.bindAll(this, 'onClick', 'hide', 'show', 'destroy');
this.state = state;
this.state.videoPlayPlaceholder = this;
this.i18n = i18n;
this.initialize();
return $.Deferred().resolve().promise();
};
PlayPlaceholder.prototype = {
destroy: function () {
this.el.off('click', this.onClick);
this.state.el.on({
'destroy': this.destroy,
'play': this.hide,
'ended pause': this.show
});
this.hide();
delete this.state.videoPlayPlaceholder;
},
/**
* Indicates whether the placeholder should be shown. We display it
* for html5 videos on iPad and Android devices.
* @return {Boolean}
*/
shouldBeShown: function () {
return /iPad|Android/i.test(this.state.isTouch[0]) && !this.state.isYoutubeType();
},
/** Initializes the module. */
initialize: function() {
if (!this.shouldBeShown()) {
return false;
}
this.el = this.state.el.find('.btn-play');
this.bindHandlers();
this.show();
},
/** Bind any necessary function callbacks to DOM events. */
bindHandlers: function() {
this.el.on('click', this.onClick);
this.state.el.on({
'destroy': this.destroy,
'play': this.hide,
'ended pause': this.show
});
},
onClick: function () {
this.state.videoCommands.execute('play');
},
hide: function () {
this.el
.addClass('is-hidden')
.attr({'aria-hidden': 'true', 'tabindex': -1});
},
show: function () {
this.el
.removeClass('is-hidden')
.attr({'aria-hidden': 'false', 'tabindex': 0});
}
};
return PlayPlaceholder;
});
}(RequireJS.define));
(function(define) {
'use strict';
define('video/09_play_skip_control.js', [], function() {
/**
* Play/skip control module.
* @exports video/09_play_skip_control.js
* @constructor
* @param {Object} state The object containing the state of the video
* @param {Object} i18n The object containing strings with translations.
* @return {jquery Promise}
*/
var PlaySkipControl = function(state, i18n) {
if (!(this instanceof PlaySkipControl)) {
return new PlaySkipControl(state, i18n);
}
_.bindAll(this, 'play', 'onClick', 'destroy');
this.state = state;
this.state.videoPlaySkipControl = this;
this.i18n = i18n;
this.initialize();
return $.Deferred().resolve().promise();
};
PlaySkipControl.prototype = {
template: [
'<a class="video_control play play-skip-control" href="#" title="',
gettext('Play'), '" role="button" aria-disabled="false">',
gettext('Play'),
'</a>'
].join(''),
destroy: function () {
this.el.remove();
this.state.el.off('destroy', this.destroy);
delete this.state.videoPlaySkipControl;
},
/** Initializes the module. */
initialize: function() {
this.el = $(this.template);
this.render();
this.bindHandlers();
},
/**
* Creates any necessary DOM elements, attach them, and set their,
* initial configuration.
*/
render: function() {
this.state.el.find('.vcr').prepend(this.el);
},
/** Bind any necessary function callbacks to DOM events. */
bindHandlers: function() {
this.el.on('click', this.onClick);
this.state.el.on({
'play': this.play,
'destroy': this.destroy
});
},
onClick: function (event) {
event.preventDefault();
if (this.state.videoPlayer.isPlaying()) {
this.state.videoCommands.execute('skip');
} else {
this.state.videoCommands.execute('play');
}
},
play: function () {
this.el
.attr('title', gettext('Skip')).text(gettext('Skip'))
.removeClass('play').addClass('skip');
// Disable possibility to pause the video.
this.state.el.find('video').off('click');
}
};
return PlaySkipControl;
});
}(RequireJS.define));
(function (define) {
'use strict';
define('video/09_poster.js', [], function () {
/**
* Poster module.
* @exports video/09_poster.js
* @constructor
* @param {jquery Element} element
* @param {Object} options
*/
var VideoPoster = function (element, options) {
if (!(this instanceof VideoPoster)) {
return new VideoPoster(element, options);
}
_.bindAll(this, 'onClick', 'destroy');
this.element = element;
this.container = element.find('.video-player');
this.options = options || {};
this.initialize();
};
VideoPoster.moduleName = 'Poster';
VideoPoster.prototype = {
template: _.template([
'<div class="video-pre-roll is-<%= type %> poster" ',
'style="background-image: url(<%= url %>)">',
'<button class="btn-play">', gettext('Play video'), '</button>',
'</div>'
].join('')),
initialize: function () {
this.el = $(this.template({
url: this.options.poster.url,
type: this.options.poster.type
}));
this.element.addClass('is-pre-roll');
this.render();
this.bindHandlers();
},
bindHandlers: function () {
this.el.on('click', this.onClick);
this.element.on('destroy', this.destroy);
},
render: function () {
this.container.append(this.el);
},
onClick: function () {
if (_.isFunction(this.options.onClick)) {
this.options.onClick();
}
this.destroy();
},
destroy: function () {
this.element.off('destroy', this.destroy).removeClass('is-pre-roll');
this.el.remove();
}
};
return VideoPoster;
});
}(RequireJS.define));
(function(define) {
'use strict';
define('video/09_save_state_plugin.js', [], function() {
/**
* Save state module.
* @exports video/09_save_state_plugin.js
* @constructor
* @param {Object} state The object containing the state of the video
* @param {Object} i18n The object containing strings with translations.
* @param {Object} options
* @return {jquery Promise}
*/
var SaveStatePlugin = function(state, i18n, options) {
if (!(this instanceof SaveStatePlugin)) {
return new SaveStatePlugin(state, i18n, options);
}
_.bindAll(this, 'onSpeedChange', 'saveStateHandler', 'bindUnloadHandler', 'onUnload', 'onYoutubeAvailability',
'onLanguageChange', 'destroy');
this.state = state;
this.options = _.extend({events: []}, options);
this.state.videoSaveStatePlugin = this;
this.i18n = i18n;
this.initialize();
return $.Deferred().resolve().promise();
};
SaveStatePlugin.moduleName = 'SaveStatePlugin';
SaveStatePlugin.prototype = {
destroy: function () {
this.state.el.off(this.events).off('destroy', this.destroy);
$(window).off('unload', this.onUnload);
delete this.state.videoSaveStatePlugin;
},
initialize: function() {
this.events = {
'speedchange': this.onSpeedChange,
'play': this.bindUnloadHandler,
'pause destroy': this.saveStateHandler,
'language_menu:change': this.onLanguageChange,
'youtube_availability': this.onYoutubeAvailability
};
this.bindHandlers();
},
bindHandlers: function() {
if (this.options.events.length) {
_.each(this.options.events, function (eventName) {
var callback;
if (_.has(this.events, eventName)) {
callback = this.events[eventName];
this.state.el.on(eventName, callback);
}
}, this);
} else {
this.state.el.on(this.events);
}
this.state.el.on('destroy', this.destroy);
},
bindUnloadHandler: _.once(function () {
$(window).on('unload.video', this.onUnload);
}),
onSpeedChange: function (event, newSpeed) {
this.saveState(true, {speed: newSpeed});
this.state.storage.setItem('speed', newSpeed, true);
this.state.storage.setItem('general_speed', newSpeed);
},
saveStateHandler: function () {
this.saveState(true);
},
onUnload: function () {
this.saveState();
},
onLanguageChange: function (event, langCode) {
this.state.storage.setItem('language', langCode);
},
onYoutubeAvailability: function (event, youtubeIsAvailable) {
this.saveState(true, {youtube_is_available: youtubeIsAvailable});
},
saveState: function (async, data) {
if (!($.isPlainObject(data))) {
data = {
saved_video_position: this.state.videoPlayer.currentTime
};
}
if (data.speed) {
this.state.storage.setItem('speed', data.speed, true);
}
if (_.has(data, 'saved_video_position')) {
this.state.storage.setItem('savedVideoPosition', data.saved_video_position, true);
data.saved_video_position = Time.formatFull(data.saved_video_position);
}
$.ajax({
url: this.state.config.saveStateUrl,
type: 'POST',
async: async ? true : false,
dataType: 'json',
data: data
});
}
};
return SaveStatePlugin;
});
}(RequireJS.define));
(function(define) {
'use strict';
// VideoSkipControl module.
define(
'video/09_skip_control.js', [],
function() {
/**
* Video skip control module.
* @exports video/09_skip_control.js
* @constructor
* @param {Object} state The object containing the state of the video
* @param {Object} i18n The object containing strings with translations.
* @return {jquery Promise}
*/
var SkipControl = function(state, i18n) {
if (!(this instanceof SkipControl)) {
return new SkipControl(state, i18n);
}
_.bindAll(this, 'onClick', 'render', 'destroy');
this.state = state;
this.state.videoSkipControl = this;
this.i18n = i18n;
this.initialize();
return $.Deferred().resolve().promise();
};
SkipControl.prototype = {
template: [
'<a class="video_control skip skip-control" href="#" title="',
gettext('Do not show again'), '" role="button" aria-disabled="false">',
gettext('Do not show again'),
'</a>'
].join(''),
destroy: function () {
this.el.remove();
this.state.el.off('.skip');
delete this.state.videoSkipControl;
},
/** Initializes the module. */
initialize: function() {
this.el = $(this.template);
this.bindHandlers();
},
/**
* Creates any necessary DOM elements, attach them, and set their,
* initial configuration.
*/
render: function() {
this.state.el.find('.vcr a').after(this.el);
},
/** Bind any necessary function callbacks to DOM events. */
bindHandlers: function() {
this.el.on('click', this.onClick);
this.state.el.on({
'play.skip': _.once(this.render),
'destroy.skip': this.destroy
});
},
onClick: function (event) {
event.preventDefault();
this.state.videoCommands.execute('skip', true);
}
};
return SkipControl;
});
}(RequireJS.define));
(function(define) { (function(define) {
'use strict'; 'use strict';
// VideoCommands module.
define('video/10_commands.js', [], function() { define('video/10_commands.js', [], function() {
var VideoCommands, Command, playCommand, pauseCommand, togglePlaybackCommand, var VideoCommands, Command, playCommand, pauseCommand, togglePlaybackCommand,
muteCommand, unmuteCommand, toggleMuteCommand, toggleFullScreenCommand, toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand, skipCommand;
setSpeedCommand;
/** /**
* Video commands module. * Video commands module.
* @exports video/10_commands.js * @exports video/10_commands.js
...@@ -19,6 +16,7 @@ define('video/10_commands.js', [], function() { ...@@ -19,6 +16,7 @@ define('video/10_commands.js', [], function() {
return new VideoCommands(state, i18n); return new VideoCommands(state, i18n);
} }
_.bindAll(this, 'destroy');
this.state = state; this.state = state;
this.state.videoCommands = this; this.state.videoCommands = this;
this.i18n = i18n; this.i18n = i18n;
...@@ -29,9 +27,15 @@ define('video/10_commands.js', [], function() { ...@@ -29,9 +27,15 @@ define('video/10_commands.js', [], function() {
}; };
VideoCommands.prototype = { VideoCommands.prototype = {
destroy: function () {
this.state.el.off('destroy', this.destroy);
delete this.state.videoCommands;
},
/** Initializes the module. */ /** Initializes the module. */
initialize: function() { initialize: function() {
this.commands = this.getCommands(); this.commands = this.getCommands();
this.state.el.on('destroy', this.destroy);
}, },
execute: function (command) { execute: function (command) {
...@@ -48,7 +52,8 @@ define('video/10_commands.js', [], function() { ...@@ -48,7 +52,8 @@ define('video/10_commands.js', [], function() {
var commands = {}, var commands = {},
commandsList = [ commandsList = [
playCommand, pauseCommand, togglePlaybackCommand, playCommand, pauseCommand, togglePlaybackCommand,
toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand,
skipCommand
]; ];
_.each(commandsList, function(command) { _.each(commandsList, function(command) {
...@@ -73,7 +78,7 @@ define('video/10_commands.js', [], function() { ...@@ -73,7 +78,7 @@ define('video/10_commands.js', [], function() {
}); });
togglePlaybackCommand = new Command('togglePlayback', function (state) { togglePlaybackCommand = new Command('togglePlayback', function (state) {
if (state.videoControl.isPlaying) { if (state.videoPlayer.isPlaying()) {
pauseCommand.execute(state); pauseCommand.execute(state);
} else { } else {
playCommand.execute(state); playCommand.execute(state);
...@@ -85,13 +90,21 @@ define('video/10_commands.js', [], function() { ...@@ -85,13 +90,21 @@ define('video/10_commands.js', [], function() {
}); });
toggleFullScreenCommand = new Command('toggleFullScreen', function (state) { toggleFullScreenCommand = new Command('toggleFullScreen', function (state) {
state.videoControl.toggleFullScreen(); state.videoFullScreen.toggle();
}); });
setSpeedCommand = new Command('speed', function (state, speed) { setSpeedCommand = new Command('speed', function (state, speed) {
state.videoSpeedControl.setSpeed(state.speedToString(speed)); state.videoSpeedControl.setSpeed(state.speedToString(speed));
}); });
skipCommand = new Command('skip', function (state, doNotShowAgain) {
if (doNotShowAgain) {
state.videoBumper.skipAndDoNotShowAgain();
} else {
state.videoBumper.skip();
}
});
return VideoCommands; return VideoCommands;
}); });
}(RequireJS.define)); }(RequireJS.define));
(function (require, $) { (function (require, $) {
'use strict'; 'use strict';
// In the case when the Video constructor will be called before RequireJS finishes loading all of the Video // In the case when the Video constructor will be called before RequireJS finishes loading all of the Video
// dependencies, we will have a mock function that will collect all the elements that must be initialized as // dependencies, we will have a mock function that will collect all the elements that must be initialized as
// Video elements. // Video elements.
...@@ -35,74 +34,122 @@ ...@@ -35,74 +34,122 @@
// Main module. // Main module.
require( require(
[ [
'video/00_video_storage.js',
'video/01_initialize.js', 'video/01_initialize.js',
'video/025_focus_grabber.js', 'video/025_focus_grabber.js',
'video/035_video_accessible_menu.js', 'video/035_video_accessible_menu.js',
'video/04_video_control.js', 'video/04_video_control.js',
'video/04_video_full_screen.js',
'video/05_video_quality_control.js', 'video/05_video_quality_control.js',
'video/06_video_progress_slider.js', 'video/06_video_progress_slider.js',
'video/07_video_volume_control.js', 'video/07_video_volume_control.js',
'video/08_video_speed_control.js', 'video/08_video_speed_control.js',
'video/09_video_caption.js', 'video/09_video_caption.js',
'video/09_play_placeholder.js',
'video/09_play_pause_control.js',
'video/09_play_skip_control.js',
'video/09_skip_control.js',
'video/09_bumper.js',
'video/09_save_state_plugin.js',
'video/09_events_plugin.js',
'video/09_events_bumper_plugin.js',
'video/09_poster.js',
'video/10_commands.js', 'video/10_commands.js',
'video/095_video_context_menu.js' 'video/095_video_context_menu.js'
], ],
function ( function (
initialize, VideoStorage, initialize, FocusGrabber, VideoAccessibleMenu, VideoControl, VideoFullScreen,
FocusGrabber, VideoQualityControl, VideoProgressSlider, VideoVolumeControl, VideoSpeedControl, VideoCaption,
VideoAccessibleMenu, VideoPlayPlaceholder, VideoPlayPauseControl, VideoPlaySkipControl, VideoSkipControl, VideoBumper,
VideoControl, VideoSaveStatePlugin, VideoEventsPlugin, VideoEventsBumperPlugin, VideoPoster, VideoCommands,
VideoQualityControl,
VideoProgressSlider,
VideoVolumeControl,
VideoSpeedControl,
VideoCaption,
VideoCommands,
VideoContextMenu VideoContextMenu
) { ) {
var youtubeXhr = null, var youtubeXhr = null,
oldVideo = window.Video; oldVideo = window.Video;
window.Video = function (element) { window.Video = function (element) {
var previousState = window.Video.previousState, var el = $(element).find('.video'),
state; id = el.attr('id').replace(/video_/, ''),
storage = VideoStorage('VideoState', id),
// Check for existance of previous state, uninitialize it if necessary, and create a new state. Store bumperMetadata = el.data('bumper-metadata'),
// new state for future invocation of this module consturctor function. mainVideoModules = [FocusGrabber, VideoControl, VideoPlayPlaceholder,
if (previousState && previousState.videoPlayer) { VideoPlayPauseControl, VideoProgressSlider, VideoSpeedControl, VideoVolumeControl,
previousState.saveState(true); VideoQualityControl, VideoFullScreen, VideoCaption, VideoCommands, VideoContextMenu,
$(window).off('unload', previousState.saveState); VideoSaveStatePlugin, VideoEventsPlugin],
bumperVideoModules = [VideoControl, VideoPlaySkipControl, VideoSkipControl,
VideoVolumeControl, VideoCaption, VideoCommands, VideoSaveStatePlugin, VideoEventsBumperPlugin],
state = {
el: el,
id: id,
metadata: el.data('metadata'),
storage: storage,
options: {},
youtubeXhr: youtubeXhr,
modules: mainVideoModules
};
var getBumperState = function (metadata) {
var bumperState = $.extend(true, {
el: el,
id: id,
storage: storage,
options: {},
youtubeXhr: youtubeXhr
}, {metadata: metadata});
bumperState.modules = bumperVideoModules;
bumperState.options = {
SaveStatePlugin: {events: ['language_menu:change']}
};
return bumperState;
};
var player = function (state) {
return function () {
_.extend(state.metadata, {autoplay: true, focusFirstControl: true});
initialize(state, element);
};
};
new VideoAccessibleMenu(el, {
storage: storage,
saveStateUrl: state.metadata.saveStateUrl
});
if (bumperMetadata) {
new VideoPoster(el, {
poster: el.data('poster'),
onClick: _.once(function () {
var mainVideoPlayer = player(state), bumper, bumperState;
if (storage.getItem('isBumperShown')) {
mainVideoPlayer();
} else {
bumperState = getBumperState(bumperMetadata);
bumper = new VideoBumper(player(bumperState), bumperState);
state.bumperState = bumperState;
bumper.getPromise().done(function () {
delete state.bumperState;
mainVideoPlayer();
});
}
})
});
} else {
initialize(state, element);
} }
state = {};
// Because this constructor can be called multiple times on a single page (when the user switches
// verticals, the page doesn't reload, but the content changes), we must will check each time if there
// is a previous copy of 'state' object. If there is, we will make sure that copy exists cleanly. We
// have to do this because when verticals switch, the code does not handle any Xmodule JS code that is
// running - it simply removes DOM elements from the page. Any functions that were running during this,
// and that will run afterwards (expecting the DOM elements to be present) must be stopped by hand.
window.Video.previousState = state;
state.modules = [
FocusGrabber,
VideoAccessibleMenu,
VideoControl,
VideoQualityControl,
VideoProgressSlider,
VideoVolumeControl,
VideoSpeedControl,
VideoCaption,
VideoCommands,
VideoContextMenu
];
state.youtubeXhr = youtubeXhr;
initialize(state, element);
if (!youtubeXhr) { if (!youtubeXhr) {
youtubeXhr = state.youtubeXhr; youtubeXhr = state.youtubeXhr;
} }
$(element).find('.video').data('video-player-state', state); el.data('video-player-state', state);
var onSequenceChange = function onSequenceChange () {
if (state && state.videoPlayer) {
state.videoPlayer.destroy();
}
$('.sequence').off('sequence:change', onSequenceChange);
};
$('.sequence').on('sequence:change', onSequenceChange);
// Because the 'state' object is only available inside this closure, we will also make it available to // 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 Video with Jasmine. // the caller by returning it. This is necessary so that we can test Video with Jasmine.
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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