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):
if not settings.FEATURES.get('ENABLE_TEAMS'):
filtered_list.append('teams_configuration')
if not settings.FEATURES.get('ENABLE_VIDEO_BUMPER'):
filtered_list.append('video_bumper')
return filtered_list
@classmethod
......
......@@ -74,6 +74,9 @@ FEATURES['ENABLE_TEAMS'] = True
# Enable custom content licensing
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 #################################
FEATURES['ENTRANCE_EXAMS'] = True
......
......@@ -163,6 +163,13 @@ FEATURES = {
# Teams feature
'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
......@@ -645,6 +652,8 @@ YOUTUBE = {
'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 #############################
......
......@@ -22,7 +22,7 @@ $a11y--blue-s1: saturate($blue,15%);
}
.a11y-menu-list {
@extend %ui-depth1;
@extend %ui-depth3;
top: 100%;
margin: 0;
padding: 0;
......
......@@ -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 {
@include clearfix();
position: relative;
......@@ -169,6 +186,7 @@ div.video {
}
object, iframe, video {
display: block;
border: none;
width: 100%;
}
......@@ -282,7 +300,7 @@ div.video {
}
}
ul.vcr {
.vcr {
float: left;
list-style: none;
margin: 0 lh() 0 0;
......@@ -293,49 +311,52 @@ div.video {
font-size: em(14);
}
li {
.video_control {
@extend %video-button;
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 {
@extend %video-button;
background-image: url('../images/vcr.png');
background-position: 15px 15px ;
background-repeat: no-repeat;
border-left: none;
box-shadow: 1px 0 0 #555;
padding: 0 lh(.75);
width: 14px;
&:focus {
@extend %ui-depth4;
position: relative;
outline: $white dotted thin;
outline-offset: -2px;
}
&:focus {
@extend %ui-depth4;
position: relative;
outline: $white dotted thin;
outline-offset: -2px;
}
&:empty {
height: 46px;
background-position: 15px 15px;
}
&:empty {
height: 46px;
background-position: 15px 15px;
}
&.play {
background-position: 17px -114px;
}
&.play {
background-position: 17px -114px;
}
&.pause {
background-position: 16px -50px;
}
&.pause {
background-position: 16px -50px;
}
div.vidtime {
font-weight: bold;
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);
}
&.skip {
background-image: none;
text-indent: 0;
width: initial;
white-space: nowrap;
}
}
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 {
background-image: url('../images/volume.png');
background-position: 10px center;
background-repeat: no-repeat;
border-left: none;
width: 30px;
height: 46px;
}
&:not(:first-child) > a {
border-left: none;
}
.volume-slider-container {
@include transition(none);
@extend %ui-depth1;
......@@ -686,8 +710,7 @@ div.video {
}
ol.subtitles {
width: 0;
height: 0;
@extend .is-hidden;
}
ol.subtitles.html5 {
......@@ -792,13 +815,38 @@ div.video {
&.is-touch {
div.tc-wrapper {
article.video-wrapper {
object, iframe, video{
object, iframe, video {
width: 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 @@
<div
id="video_id"
class="video closed"
data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl"
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"
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/"}'
>
<div class="focus_grabber first"></div>
......@@ -35,35 +20,11 @@
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#" title="Play" role="button" aria-disabled="false"></a></li>
<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 class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
<div class="secondary-controls"></div>
</div>
</section>
</article>
<ol class="subtitles"><li></li></ol>
</div>
<div class="focus_grabber last"></div>
......
......@@ -4,23 +4,7 @@
<div
id="video_id"
class="video closed"
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-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"
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/"}'
>
<div class="focus_grabber first"></div>
......@@ -36,35 +20,11 @@
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#" title="Play" role="button" aria-disabled="false"></a></li>
<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 class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
<div class="secondary-controls"></div>
</div>
</section>
</article>
<ol class="subtitles"><li></li></ol>
</div>
<div class="focus_grabber last"></div>
......
......@@ -4,23 +4,7 @@
<div
id="video_id"
class="video closed"
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-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"
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"]}'
>
<div class="focus_grabber first"></div>
......@@ -33,8 +17,6 @@
</section>
<section class="video-controls is-hidden"></section>
</article>
<ol class="subtitles"><li></li></ol>
</div>
<div class="focus_grabber last"></div>
......
......@@ -4,22 +4,7 @@
<div
id="video_id"
class="video closed"
data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl"
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"
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/"}'
>
<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 @@
<div
id="video_id1"
class="video closed"
data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl"
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"
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/"}'
>
<div class="focus_grabber first"></div>
......@@ -35,35 +20,11 @@
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#" title="Play" role="button" aria-disabled="false"></a></li>
<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 class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
<div class="secondary-controls"></div>
</div>
</section>
</article>
<ol class="subtitles"><li></li></ol>
</div>
<div class="focus_grabber last"></div>
......@@ -77,20 +38,7 @@
<div
id="video_id2"
class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
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"
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/"}'
>
<div class="tc-wrapper">
<article class="video-wrapper">
......@@ -102,30 +50,8 @@
<section class="video-controls">
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#" title="Play"></a></li>
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a 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 class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
<div class="secondary-controls"></div>
</div>
</section>
</article>
......@@ -142,20 +68,7 @@
<div
id="video_id3"
class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
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"
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/"}'
>
<div class="tc-wrapper">
<article class="video-wrapper">
......
......@@ -206,6 +206,9 @@
},
toBeInArray: function (array) {
return $.inArray(this.actual, array) > -1;
},
toBeFocused: function () {
return $(this.actual)[0] === $(this.actual)[0].ownerDocument.activeElement;
}
});
......@@ -239,12 +242,11 @@
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.
if (_.isObject(params)) {
$('#example')
.find('#video_id')
.data(params);
var metadata = _.extend($('#video_id').data('metadata'), params);
$('#video_id').data('metadata', metadata);
}
jasmine.stubRequests();
......
(function (undefined) {
describe('Video', function () {
var oldOTBD;
var oldOTBD, state;
beforeEach(function () {
jasmine.stubRequests();
......@@ -17,11 +17,12 @@
beforeEach(function () {
loadFixtures('video.html');
$.cookie.andReturn('0.50');
this.state = jasmine.initializePlayerYouTube('video_html5.html');
});
describe('by default', function () {
beforeEach(function () {
this.state = new window.Video('#example');
afterEach(function () {
this.state.videoPlayer.destroy();
});
it('check videoType', function () {
......@@ -54,19 +55,16 @@
var state;
beforeEach(function () {
loadFixtures('video_html5.html');
$.cookie.andReturn('0.75');
state = jasmine.initializePlayer('video_html5.html');
});
describe('by default', function () {
beforeEach(function () {
state = new window.Video('#example');
});
afterEach(function () {
state = undefined;
});
afterEach(function () {
state.videoPlayer.destroy();
state = undefined;
});
describe('by default', function () {
it('check videoType', function () {
expect(state.videoType).toEqual('html5');
});
......@@ -95,14 +93,6 @@
// the stand alone HTML5 player object is already loaded, so no
// further testing in that case is required.
describe('HTML5 API is available', function () {
beforeEach(function () {
state = new Video('#example');
});
afterEach(function () {
state = null;
});
it('create the Video Player', function () {
expect(state.videoPlayer.player).not.toBeUndefined();
});
......@@ -113,8 +103,11 @@
describe('YouTube API is not loaded', function () {
beforeEach(function () {
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 () {
......@@ -159,9 +152,8 @@
}
];
beforeEach(function () {
loadFixtures('video.html');
afterEach(function () {
state.videoPlayer.destroy();
});
$.each(miniTestSuite, function (index, test) {
......@@ -172,13 +164,10 @@
function itFabrique(itDescription, data, expectData) {
it(itDescription, function () {
$('#example').find('.video')
.data({
'start': data.start,
'end': data.end
});
state = new Video('#example');
state = jasmine.initializePlayer('video.html', {
'start': data.start,
'end': data.end
});
expect(state.config.startTime).toBe(expectData.start);
expect(state.config.endTime).toBe(expectData.end);
......@@ -238,26 +227,5 @@
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);
......@@ -10,8 +10,8 @@
afterEach(function () {
state.storage.clear();
state.videoPlayer.destroy();
$.fn.scrollTo.reset();
$('.subtitles').remove();
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
});
......
......@@ -12,158 +12,6 @@ function (Initialize) {
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 () {
var msg;
......@@ -356,20 +204,12 @@ function (Initialize) {
describe('when new speed is available', function () {
beforeEach(function () {
Initialize.prototype.setSpeed.call(state, '0.75', true);
Initialize.prototype.setSpeed.call(state, '0.75');
});
it('set new speed', function () {
expect(state.speed).toEqual('0.75');
});
it('save setting for new speed', function () {
expect(state.storage.setItem.calls[0].args)
.toEqual(['speed', '0.75', true]);
expect(state.storage.setItem.calls[1].args)
.toEqual(['general_speed', '0.75']);
});
});
describe('when new speed is not available', function () {
......@@ -390,7 +230,7 @@ function (Initialize) {
};
$.each(map, function(key, expected) {
Initialize.prototype.setSpeed.call(state, key, true);
Initialize.prototype.setSpeed.call(state, key);
expect(state.speed).toBe(expected);
});
});
......
......@@ -5,6 +5,7 @@
afterEach(function () {
$('source').remove();
state.storage.clear();
state.videoPlayer.destroy();
});
describe('constructor', function () {
......@@ -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 () {
......
(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 @@
});
afterEach(function () {
$('.subtitles').remove();
// `source` tags should be removed to avoid memory leak bug that we
// had before. Removing of `source` tag, not `video` tag, stops
// loading video source and clears the memory.
$('source').remove();
$.fn.scrollTo.reset();
state.storage.clear();
state.videoPlayer.destroy();
window.onTouchBasedDevice = oldOTBD;
});
......@@ -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 () {
state = jasmine.initializePlayer();
expect($('.subtitles')).toHandle('mouseover');
......@@ -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('is rendered', function () {
it('if languages more than 1', function () {
......@@ -593,7 +608,7 @@
it(msg, function () {
spyOn(Caption, 'fetchAvailableTranslations');
$.ajax.andCallFake(function (settings) {
settings.error([]);
_.result(settings, 'error');
});
state.config.transcriptLanguages = {};
......@@ -612,7 +627,7 @@
xit(msg, function () {
$.ajax
.andCallFake(function (settings) {
settings.error([]);
_.result(settings, 'error');
});
state.config.transcriptLanguages = {
......@@ -690,7 +705,7 @@
msg = 'on error: captions are hidden if there are no transcript';
it(msg, function () {
$.ajax.andCallFake(function (settings) {
settings.error();
_.result(settings, 'error');
});
Caption.fetchAvailableTranslations();
......@@ -907,8 +922,8 @@
$('.subtitles').css('maxHeight'), 10
);
videoWrapperHeight = $('.video-wrapper').height();
progressSliderHeight = videoControl.sliderEl.height();
controlHeight = videoControl.el.height();
progressSliderHeight = state.el.find('.slider').height();
controlHeight = state.el.find('.video-controls').height();
shouldBeHeight = videoWrapperHeight -
0.5 * progressSliderHeight -
controlHeight;
......@@ -1043,7 +1058,6 @@
describe('toggle', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
spyOn(state.videoPlayer, 'log');
$('.subtitles li[data-index=1]').addClass('current');
});
......@@ -1053,15 +1067,6 @@
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 () {
expect(state.el).toHaveClass('closed');
});
......@@ -1079,15 +1084,6 @@
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 () {
expect(state.el).not.toHaveClass('closed');
});
......
......@@ -68,6 +68,7 @@
$('source').remove();
_.result(state.storage, 'clear');
_.result($('video').data('contextmenu'), 'destroy');
_.result(state.videoPlayer, 'destroy');
});
describe('constructor', function () {
......@@ -219,12 +220,13 @@
it('mouse left/right-clicking behaves as expected on play/pause menu item', function () {
var menuItem = menuItems.first();
spyOn(state.videoPlayer, 'isPlaying');
spyOn(state.videoPlayer, 'play').andCallFake(function () {
state.videoControl.isPlaying = true;
state.videoPlayer.isPlaying.andReturn(true);
state.el.trigger('play');
});
spyOn(state.videoPlayer, 'pause').andCallFake(function () {
state.videoControl.isPlaying = false;
state.videoPlayer.isPlaying.andReturn(false);
state.el.trigger('pause');
});
// 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 @@
afterEach(function () {
// Turn jQuery animations back on.
jQuery.fx.off = true;
state.videoPlayer.destroy();
});
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 @@
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
state.storage.clear();
state.videoPlayer.destroy();
});
describe('constructor', function () {
......@@ -38,6 +39,18 @@
expect(state.videoProgressSlider.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 () {
......@@ -304,6 +317,13 @@
});
});
it('can destroy itself', function () {
state = jasmine.initializePlayer();
state.videoProgressSlider.destroy();
expect(state.videoProgressSlider).toBeUndefined();
expect($('.slider')).toBeEmpty();
});
});
}).call(this);
(function (undefined) {
describe('VideoQualityControl', function () {
var state, qualityControl, qualityControlEl, videoPlayer, player;
var state, qualityControl, videoPlayer, player;
afterEach(function () {
$('source').remove();
if (state.storage) {
state.storage.clear();
}
state.videoPlayer.destroy();
});
describe('constructor, YouTube mode', function () {
......@@ -105,6 +106,11 @@
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 () {
......
(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 @@
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
state.storage.clear();
state.videoPlayer.destroy();
});
describe('constructor', function () {
......@@ -247,5 +248,13 @@
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);
......@@ -13,6 +13,7 @@ describe('VideoVolumeControl', function () {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
state.storage.clear();
state.videoPlayer.destroy();
});
it('Volume level has correct value even if cookie is broken', function () {
......@@ -35,8 +36,7 @@ describe('VideoVolumeControl', function () {
});
it('render the volume control', function () {
expect(state.videoControl.secondaryControlsEl.html())
.toContain('<div class="volume">\n');
expect($('.volume')).toExist();
});
it('create the slider', function () {
......@@ -292,7 +292,7 @@ describe('VideoVolumeControl', function () {
shiftKey: true
});
});
})
});
describe('keyDownButtonHandler', function () {
beforeEach(function () {
......@@ -308,6 +308,6 @@ describe('VideoVolumeControl', function () {
}));
expect(volumeControl.getMuteStatus()).toEqual(isMuted);
});
})
});
});
}).call(this);
......@@ -177,9 +177,8 @@ function () {
}
};
var cleanDelta = function () {
delta['height'] = 0;
delta['width'] = 0;
var resetDelta = function () {
delta['height'] = delta['width'] = 0;
return module;
};
......@@ -200,12 +199,23 @@ function () {
return module;
};
var destroy = function () {
var data = getData();
data.element.css({
'height': '', 'width': '', 'top': '', 'left': ''
});
removeCallbacks();
resetDelta();
mode = null;
};
initialize.apply(module, arguments);
return $.extend(true, module, {
align: align,
alignByWidthOnly: alignByWidthOnly,
alignByHeightOnly: alignByHeightOnly,
destroy: destroy,
setParams: initialize,
setMode: setMode,
setElement: setElement,
......@@ -218,7 +228,7 @@ function () {
delta: {
add: addDelta,
substract: substractDelta,
reset: cleanDelta
reset: resetDelta
}
});
};
......
......@@ -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;
/*
......@@ -152,6 +200,7 @@ function () {
var isTouch = onTouchBasedDevice() || '',
sourceList, _this, errorMessage, lastSource;
_.bindAll(this, 'onLoadedMetadata', 'onPlay', 'onPlaying', 'onPause', 'onEnded');
this.logs = [];
// 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
......@@ -226,6 +275,8 @@ function () {
lastSource = this.videoEl.find('source').last();
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])) {
this.videoEl.prop('controls', true);
......@@ -280,35 +331,11 @@ function () {
// When the <video> tag has been processed by the browser, and it
// is ready for playback, notify other parts of the VideoPlayer,
// and initially pause the video.
this.video.addEventListener('loadedmetadata', function () {
_this.playerState = HTML5Video.PlayerState.PAUSED;
if ($.isFunction(_this.config.events.onReady)) {
_this.config.events.onReady(null);
}
}, 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);
this.video.addEventListener('loadedmetadata', this.onLoadedMetadata, false);
this.video.addEventListener('play', this.onPlay, false);
this.video.addEventListener('playing', this.onPlaying, false);
this.video.addEventListener('pause', this.onPause, false);
this.video.addEventListener('ended', this.onEnded, false);
// Place the <video> element on the page.
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(
'video/05_video_quality_control.js',
[],
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".
return function (state) {
......@@ -12,7 +18,6 @@ function () {
// Changing quality for now only works for YouTube videos.
if (state.videoType !== 'youtube') {
state.el.find('a.quality-control').remove();
return;
}
......@@ -36,6 +41,7 @@ function () {
// get the 'state' object as a context.
function _makeFunctionsPublic(state) {
var methodsDict = {
destroy: destroy,
fetchAvailableQualities: fetchAvailableQualities,
onQualityChange: onQualityChange,
showQualityControl: showQualityControl,
......@@ -45,16 +51,25 @@ function () {
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)
//
// 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.videoQualityControl.el = state.el.find('a.quality-control');
state.videoQualityControl.el.show();
var element = state.videoQualityControl.el = $(template);
state.videoQualityControl.quality = 'large';
state.el.find('.secondary-controls').append(element);
}
// function _bindHandlers(state)
......@@ -64,9 +79,11 @@ function () {
state.videoQualityControl.el.on('click',
state.videoQualityControl.toggleQuality
);
state.el.on('play', _.once(
state.el.on('play.quality', _.once(
state.videoQualityControl.fetchAvailableQualities
));
state.el.on('destroy.quality', state.videoQualityControl.destroy);
}
// ***************************************************************
......@@ -141,7 +158,7 @@ function () {
event.preventDefault();
newQuality = isHD ? 'large' : 'highres';
this.trigger('videoPlayer.handlePlaybackQualityChange', newQuality);
}
......
......@@ -12,15 +12,17 @@ define(
'video/06_video_progress_slider.js',
[],
function () {
var template = [
'<div class="slider" title="', gettext('Video position'), '"></div>'
].join('');
// VideoProgressSlider() function - what this module "exports".
return function (state) {
var dfd = $.Deferred();
state.videoProgressSlider = {};
_makeFunctionsPublic(state);
_renderElements(state);
// No callbacks to DOM events (click, mousemove, etc.).
dfd.resolve();
return dfd.promise();
......@@ -36,6 +38,7 @@ function () {
// these functions will get the 'state' object as a context.
function _makeFunctionsPublic(state) {
var methodsDict = {
destroy: destroy,
buildSlider: buildSlider,
getRangeParams: getRangeParams,
onSlide: onSlide,
......@@ -49,6 +52,12 @@ function () {
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)
//
// Create any necessary DOM elements, attach them, and set their
......@@ -56,8 +65,9 @@ function () {
// via the 'state' object. Much easier to work this way - you don't
// have to do repeated jQuery element selects.
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();
_buildHandle(state);
}
......@@ -81,6 +91,8 @@ function () {
'aria-valuemin': '0',
'aria-valuenow': state.videoPlayer.currentTime
});
state.el.on('destroy', state.videoProgressSlider.destroy);
}
// ***************************************************************
......@@ -109,7 +121,7 @@ function () {
// whole slider). Remember that endTime === null means the end-time
// is set to the end of video by default.
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.
// It also must be non-zero.
......
......@@ -17,6 +17,10 @@ function() {
return new VolumeControl(state, i18n);
}
_.bindAll(this, 'keyDownHandler', 'updateVolumeSilently',
'onVolumeChangeHandler', 'openMenu', 'closeMenu',
'toggleMuteHandler', 'keyDownButtonHandler', 'destroy'
);
this.state = state;
this.state.videoVolumeControl = this;
this.i18n = i18n;
......@@ -33,17 +37,55 @@ function() {
/** Step to increase/decrease volume level via keyboard. */
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. */
initialize: function() {
var volume;
this.el = this.state.el.find('.volume');
if (this.state.isTouch) {
// iOS doesn't support volume change
this.el.remove();
return false;
}
this.el = $(this.template);
// Youtube iframe react on key buttons and has his own handlers.
// So, we disallow focusing on iframe.
this.state.el.find('iframe').attr('tabindex', -1);
......@@ -80,26 +122,28 @@ function() {
// Therefore, we do not need redundant focusing on slider in TAB
// order.
container.find('a').attr('tabindex', -1);
this.state.el.find('.secondary-controls').append(this.el);
},
/** Bind any necessary function callbacks to DOM events. */
bindHandlers: function() {
this.state.el.on({
'keydown': this.keyDownHandler.bind(this),
'play': _.once(this.updateVolumeSilently.bind(this)),
'volumechange': this.onVolumeChangeHandler.bind(this)
'keydown': this.keyDownHandler,
'play.volume': _.once(this.updateVolumeSilently),
'volumechange': this.onVolumeChangeHandler
});
this.el.on({
'mouseenter': this.openMenu.bind(this),
'mouseleave': this.closeMenu.bind(this)
'mouseenter': this.openMenu,
'mouseleave': this.closeMenu
});
this.button.on({
'click': false,
'mousedown': this.toggleMuteHandler.bind(this),
'keydown': this.keyDownButtonHandler.bind(this),
'focus': this.openMenu.bind(this),
'blur': this.closeMenu.bind(this)
'mousedown': this.toggleMuteHandler,
'keydown': this.keyDownButtonHandler,
'focus': this.openMenu,
'blur': this.closeMenu
});
this.state.el.on('destroy', this.destroy);
},
/**
......@@ -343,6 +387,10 @@ function() {
};
Accessibility.prototype = {
destroy: function () {
this.liveRegion.remove();
},
/** Initializes the module. */
initialize: function() {
this.liveRegion = $('<div />', {
......
......@@ -16,6 +16,10 @@ function (Iterator) {
return new SpeedControl(state);
}
_.bindAll(this, 'onSetSpeed', 'onRenderSpeed', 'clickLinkHandler',
'keyDownLinkHandler', 'mouseEnterHandler', 'mouseLeaveHandler',
'clickMenuHandler', 'keyDownMenuHandler', 'destroy'
);
this.state = state;
this.state.videoSpeedControl = this;
this.initialize();
......@@ -24,24 +28,51 @@ function (Iterator) {
};
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. */
initialize: function () {
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)) {
this.el.remove();
console.log(
'[Video info]: playbackRate is not supported.'
);
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.setSpeed(state.speed, true, true);
this.bindHandlers();
return true;
......@@ -51,13 +82,11 @@ function (Iterator) {
* Creates any necessary DOM elements, attach them, and set their,
* initial configuration.
* @param {array} speeds List of speeds available for the player.
* @param {string|number} currentSpeed Current speed for the player.
*/
render: function (speeds, currentSpeed) {
var self = this,
speedsContainer = this.speedsContainer,
render: function (speeds) {
var speedsContainer = this.speedsContainer,
reversedSpeeds = speeds.concat().reverse(),
speedsList = $.map(reversedSpeeds, function (speed, index) {
speedsList = $.map(reversedSpeeds, function (speed) {
return [
'<li data-speed="', speed, '" role="presentation">',
'<a class="speed-link" href="#" role="menuitem" tabindex="-1">',
......@@ -69,7 +98,7 @@ function (Iterator) {
speedsContainer.html(speedsList.join(''));
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) {
* mousemove, etc.).
*/
bindHandlers: function () {
var self = this;
// Attach various events handlers to the speed menu button.
this.el.on({
'mouseenter': this.mouseEnterHandler.bind(this),
'mouseleave': this.mouseLeaveHandler.bind(this),
'click': this.clickMenuHandler.bind(this),
'keydown': this.keyDownMenuHandler.bind(this)
'mouseenter': this.mouseEnterHandler,
'mouseleave': this.mouseLeaveHandler,
'click': this.clickMenuHandler,
'keydown': this.keyDownMenuHandler
});
// Attach click and keydown event handlers to the individual speed
// entries.
this.speedsContainer.on({
click: this.clickLinkHandler.bind(this),
keydown: this.keyDownLinkHandler.bind(this)
click: this.clickLinkHandler,
keydown: this.keyDownLinkHandler
}, 'a.speed-link');
this.state.el.on({
'speed:set': function (event, speed) {
self.setSpeed(speed, true);
},
'speed:render': function (event, speeds, currentSpeed) {
self.render(speeds, currentSpeed);
}
'speed:set': this.onSetSpeed,
'speed:render': this.onRenderSpeed
});
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) {
// element to have clicks close the menu when they happen
// outside of it.
if (bindEvent) {
$(window).on('click.speedMenu', this.clickMenuHandler.bind(this));
$(window).on('click.speedMenu', this.clickMenuHandler);
}
this.el.addClass('is-opened');
......@@ -175,7 +207,7 @@ function (Iterator) {
this.currentSpeed = speed;
if (!silent) {
this.el.trigger('speedchange', [speed]);
this.el.trigger('speedchange', [speed, this.state.speed]);
}
}
},
......
......@@ -656,6 +656,12 @@ function (Component) {
if (!state.isYoutubeType()) {
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();
......
(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) {
'use strict';
// VideoCommands module.
define('video/10_commands.js', [], function() {
var VideoCommands, Command, playCommand, pauseCommand, togglePlaybackCommand,
muteCommand, unmuteCommand, toggleMuteCommand, toggleFullScreenCommand,
setSpeedCommand;
toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand, skipCommand;
/**
* Video commands module.
* @exports video/10_commands.js
......@@ -19,6 +16,7 @@ define('video/10_commands.js', [], function() {
return new VideoCommands(state, i18n);
}
_.bindAll(this, 'destroy');
this.state = state;
this.state.videoCommands = this;
this.i18n = i18n;
......@@ -29,9 +27,15 @@ define('video/10_commands.js', [], function() {
};
VideoCommands.prototype = {
destroy: function () {
this.state.el.off('destroy', this.destroy);
delete this.state.videoCommands;
},
/** Initializes the module. */
initialize: function() {
this.commands = this.getCommands();
this.state.el.on('destroy', this.destroy);
},
execute: function (command) {
......@@ -48,7 +52,8 @@ define('video/10_commands.js', [], function() {
var commands = {},
commandsList = [
playCommand, pauseCommand, togglePlaybackCommand,
toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand
toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand,
skipCommand
];
_.each(commandsList, function(command) {
......@@ -73,7 +78,7 @@ define('video/10_commands.js', [], function() {
});
togglePlaybackCommand = new Command('togglePlayback', function (state) {
if (state.videoControl.isPlaying) {
if (state.videoPlayer.isPlaying()) {
pauseCommand.execute(state);
} else {
playCommand.execute(state);
......@@ -85,13 +90,21 @@ define('video/10_commands.js', [], function() {
});
toggleFullScreenCommand = new Command('toggleFullScreen', function (state) {
state.videoControl.toggleFullScreen();
state.videoFullScreen.toggle();
});
setSpeedCommand = new Command('speed', function (state, 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;
});
}(RequireJS.define));
(function (require, $) {
'use strict';
// 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
// Video elements.
......@@ -35,74 +34,122 @@
// Main module.
require(
[
'video/00_video_storage.js',
'video/01_initialize.js',
'video/025_focus_grabber.js',
'video/035_video_accessible_menu.js',
'video/04_video_control.js',
'video/04_video_full_screen.js',
'video/05_video_quality_control.js',
'video/06_video_progress_slider.js',
'video/07_video_volume_control.js',
'video/08_video_speed_control.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/095_video_context_menu.js'
],
function (
initialize,
FocusGrabber,
VideoAccessibleMenu,
VideoControl,
VideoQualityControl,
VideoProgressSlider,
VideoVolumeControl,
VideoSpeedControl,
VideoCaption,
VideoCommands,
VideoStorage, initialize, FocusGrabber, VideoAccessibleMenu, VideoControl, VideoFullScreen,
VideoQualityControl, VideoProgressSlider, VideoVolumeControl, VideoSpeedControl, VideoCaption,
VideoPlayPlaceholder, VideoPlayPauseControl, VideoPlaySkipControl, VideoSkipControl, VideoBumper,
VideoSaveStatePlugin, VideoEventsPlugin, VideoEventsBumperPlugin, VideoPoster, VideoCommands,
VideoContextMenu
) {
var youtubeXhr = null,
oldVideo = window.Video;
window.Video = function (element) {
var previousState = window.Video.previousState,
state;
// Check for existance of previous state, uninitialize it if necessary, and create a new state. Store
// new state for future invocation of this module consturctor function.
if (previousState && previousState.videoPlayer) {
previousState.saveState(true);
$(window).off('unload', previousState.saveState);
var el = $(element).find('.video'),
id = el.attr('id').replace(/video_/, ''),
storage = VideoStorage('VideoState', id),
bumperMetadata = el.data('bumper-metadata'),
mainVideoModules = [FocusGrabber, VideoControl, VideoPlayPlaceholder,
VideoPlayPauseControl, VideoProgressSlider, VideoSpeedControl, VideoVolumeControl,
VideoQualityControl, VideoFullScreen, VideoCaption, VideoCommands, VideoContextMenu,
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) {
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
// 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