Commit ef8f3918 by Alexander Kryklia

Merge pull request #7825 from edx/alex/video_bumper

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