@@ -79,6 +79,9 @@ class CourseMetadata(object):
|
||||
if not settings.FEATURES.get('ENABLE_TEAMS'):
|
||||
filtered_list.append('teams_configuration')
|
||||
|
||||
if not settings.FEATURES.get('ENABLE_VIDEO_BUMPER'):
|
||||
filtered_list.append('video_bumper')
|
||||
|
||||
return filtered_list
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -74,6 +74,9 @@ FEATURES['ENABLE_TEAMS'] = True
|
||||
# Enable custom content licensing
|
||||
FEATURES['LICENSING'] = True
|
||||
|
||||
FEATURES['ENABLE_MOBILE_REST_API'] = True # Enable video bumper in Studio
|
||||
FEATURES['ENABLE_VIDEO_BUMPER'] = True # Enable video bumper in Studio settings
|
||||
|
||||
########################### Entrance Exams #################################
|
||||
FEATURES['ENTRANCE_EXAMS'] = True
|
||||
|
||||
|
||||
@@ -163,6 +163,13 @@ FEATURES = {
|
||||
|
||||
# Teams feature
|
||||
'ENABLE_TEAMS': False,
|
||||
|
||||
# Show video bumper in Studio
|
||||
'ENABLE_VIDEO_BUMPER': False,
|
||||
|
||||
# How many seconds to show the bumper again, default is 7 days:
|
||||
'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600,
|
||||
|
||||
}
|
||||
|
||||
ENABLE_JASMINE = False
|
||||
@@ -645,6 +652,8 @@ YOUTUBE = {
|
||||
'v': 'set_youtube_id_of_11_symbols_here',
|
||||
},
|
||||
},
|
||||
|
||||
'IMAGE_API': 'http://img.youtube.com/vi/{youtube_id}/0.jpg', # /maxresdefault.jpg for 1920*1080
|
||||
}
|
||||
|
||||
############################# VIDEO UPLOAD PIPELINE #############################
|
||||
|
||||
@@ -22,7 +22,7 @@ $a11y--blue-s1: saturate($blue,15%);
|
||||
}
|
||||
|
||||
.a11y-menu-list {
|
||||
@extend %ui-depth1;
|
||||
@extend %ui-depth3;
|
||||
top: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -27,6 +27,23 @@ div.video {
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: video pre-roll state
|
||||
&.is-pre-roll {
|
||||
.slider {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
position: relative;
|
||||
&:before {
|
||||
display: block;
|
||||
content: "";
|
||||
width: 100%;
|
||||
padding-top: 55%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.tc-wrapper {
|
||||
@include clearfix();
|
||||
position: relative;
|
||||
@@ -169,6 +186,7 @@ div.video {
|
||||
}
|
||||
|
||||
object, iframe, video {
|
||||
display: block;
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -282,7 +300,7 @@ div.video {
|
||||
}
|
||||
}
|
||||
|
||||
ul.vcr {
|
||||
.vcr {
|
||||
float: left;
|
||||
list-style: none;
|
||||
margin: 0 lh() 0 0;
|
||||
@@ -293,49 +311,52 @@ div.video {
|
||||
font-size: em(14);
|
||||
}
|
||||
|
||||
li {
|
||||
.video_control {
|
||||
@extend %video-button;
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
background-image: url('../images/vcr.png');
|
||||
background-position: 15px 15px ;
|
||||
background-repeat: no-repeat;
|
||||
border-left: none;
|
||||
padding: 0 lh(.75);
|
||||
width: 14px;
|
||||
|
||||
a {
|
||||
@extend %video-button;
|
||||
background-image: url('../images/vcr.png');
|
||||
background-position: 15px 15px ;
|
||||
background-repeat: no-repeat;
|
||||
border-left: none;
|
||||
box-shadow: 1px 0 0 #555;
|
||||
padding: 0 lh(.75);
|
||||
width: 14px;
|
||||
|
||||
&:focus {
|
||||
@extend %ui-depth4;
|
||||
position: relative;
|
||||
outline: $white dotted thin;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
height: 46px;
|
||||
background-position: 15px 15px;
|
||||
}
|
||||
|
||||
&.play {
|
||||
background-position: 17px -114px;
|
||||
}
|
||||
|
||||
&.pause {
|
||||
background-position: 16px -50px;
|
||||
}
|
||||
&:focus {
|
||||
@extend %ui-depth4;
|
||||
position: relative;
|
||||
outline: $white dotted thin;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
div.vidtime {
|
||||
font-weight: bold;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
-webkit-font-smoothing: antialiased;
|
||||
padding-left: lh(.75);
|
||||
@media (max-width: 1120px) {
|
||||
padding-left: lh(0.5);
|
||||
}
|
||||
&:empty {
|
||||
height: 46px;
|
||||
background-position: 15px 15px;
|
||||
}
|
||||
|
||||
&.play {
|
||||
background-position: 17px -114px;
|
||||
}
|
||||
|
||||
&.pause {
|
||||
background-position: 16px -50px;
|
||||
}
|
||||
|
||||
&.skip {
|
||||
background-image: none;
|
||||
text-indent: 0;
|
||||
width: initial;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
div.vidtime {
|
||||
@extend %t-strong;
|
||||
float: left;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
-webkit-font-smoothing: antialiased;
|
||||
padding-left: lh(.75);
|
||||
@media (max-width: 1120px) {
|
||||
padding-left: lh(0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -504,11 +525,14 @@ div.video {
|
||||
background-image: url('../images/volume.png');
|
||||
background-position: 10px center;
|
||||
background-repeat: no-repeat;
|
||||
border-left: none;
|
||||
width: 30px;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
&:not(:first-child) > a {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.volume-slider-container {
|
||||
@include transition(none);
|
||||
@extend %ui-depth1;
|
||||
@@ -686,8 +710,7 @@ div.video {
|
||||
}
|
||||
|
||||
ol.subtitles {
|
||||
width: 0;
|
||||
height: 0;
|
||||
@extend .is-hidden;
|
||||
}
|
||||
|
||||
ol.subtitles.html5 {
|
||||
@@ -792,13 +815,38 @@ div.video {
|
||||
&.is-touch {
|
||||
div.tc-wrapper {
|
||||
article.video-wrapper {
|
||||
object, iframe, video{
|
||||
object, iframe, video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-pre-roll {
|
||||
@extend %ui-depth3;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
background-color: $black;
|
||||
|
||||
&.is-html5 {
|
||||
background-size: 15%;
|
||||
}
|
||||
|
||||
.btn-play {
|
||||
text-indent: -999px;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
BIN
common/lib/xmodule/xmodule/js/fixtures/poster.jpg
Normal file
BIN
common/lib/xmodule/xmodule/js/fixtures/poster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -4,22 +4,7 @@
|
||||
<div
|
||||
id="video_id"
|
||||
class="video closed"
|
||||
data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl"
|
||||
data-show-captions="true"
|
||||
data-save-state-url="/save_user_state"
|
||||
data-speed="1.5"
|
||||
data-start=""
|
||||
data-end=""
|
||||
data-saved-video-position="0"
|
||||
data-transcript-language="en"
|
||||
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
|
||||
data-transcript-translation-url="/transcript/translation"
|
||||
data-transcript-available-translations-url="/transcript/available_translations"
|
||||
data-autoplay="False"
|
||||
data-yt-test-timeout="1500"
|
||||
data-yt-api-url="www.youtube.com/iframe_api"
|
||||
data-yt-test-url="gdata.youtube.com/feeds/api/videos/"
|
||||
data-autohide-html5="True"
|
||||
data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": "[]", "speed": "1.5", "startTime": "", "streams": "0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}'
|
||||
>
|
||||
<div class="focus_grabber first"></div>
|
||||
|
||||
@@ -35,35 +20,11 @@
|
||||
<section class="video-controls is-hidden">
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
<li><a class="video_control" href="#" title="Play" role="button" aria-disabled="false"></a></li>
|
||||
<li><div class="vidtime">0:00 / 0:00</div></li>
|
||||
</ul>
|
||||
<div class="secondary-controls">
|
||||
<div class="speeds">
|
||||
<a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false">
|
||||
<span class="label">Speed</span>
|
||||
<span class="value"></span>
|
||||
</a>
|
||||
<ol class="video-speeds"></ol>
|
||||
</div>
|
||||
<div class="volume">
|
||||
<a href="#" title="Volume" role="button" aria-disabled="false"></a>
|
||||
<div class="volume-slider-container">
|
||||
<div class="volume-slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
|
||||
<a href="#" class="quality-control is-hidden" title="HD off" role="button" aria-disabled="false">HD off</a>
|
||||
<div class="lang menu-container">
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
|
||||
<div class="secondary-controls"></div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<ol class="subtitles"><li></li></ol>
|
||||
</div>
|
||||
|
||||
<div class="focus_grabber last"></div>
|
||||
|
||||
@@ -4,23 +4,7 @@
|
||||
<div
|
||||
id="video_id"
|
||||
class="video closed"
|
||||
data-show-captions="true"
|
||||
data-save-state-url="/save_user_state"
|
||||
data-speed="1.5"
|
||||
data-start=""
|
||||
data-end=""
|
||||
data-saved-video-position="0"
|
||||
data-transcript-language="en"
|
||||
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
|
||||
data-transcript-translation-url="/transcript/translation"
|
||||
data-transcript-available-translations-url="/transcript/available_translations"
|
||||
data-sub="Z5KLxerq05Y"
|
||||
data-sources='["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"]'
|
||||
data-autoplay="False"
|
||||
data-yt-test-timeout="1500"
|
||||
data-yt-api-url="www.youtube.com/iframe_api"
|
||||
data-yt-test-url="gdata.youtube.com/feeds/api/videos/"
|
||||
data-autohide-html5="True"
|
||||
data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}'
|
||||
>
|
||||
<div class="focus_grabber first"></div>
|
||||
|
||||
@@ -36,35 +20,11 @@
|
||||
<section class="video-controls is-hidden">
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
<li><a class="video_control" href="#" title="Play" role="button" aria-disabled="false"></a></li>
|
||||
<li><div class="vidtime">0:00 / 0:00</div></li>
|
||||
</ul>
|
||||
<div class="secondary-controls">
|
||||
<div class="speeds">
|
||||
<a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false">
|
||||
<span class="label">Speed</span>
|
||||
<span class="value"></span>
|
||||
</a>
|
||||
<ol class="video-speeds"></ol>
|
||||
</div>
|
||||
<div class="volume">
|
||||
<a href="#" title="Volume" role="button" aria-disabled="false"></a>
|
||||
<div class="volume-slider-container">
|
||||
<div class="volume-slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
|
||||
<a href="#" class="quality-control is-hidden" title="HD off" role="button" aria-disabled="false">HD off</a>
|
||||
<div class="lang menu-container">
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
|
||||
<div class="secondary-controls"></div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<ol class="subtitles"><li></li></ol>
|
||||
</div>
|
||||
|
||||
<div class="focus_grabber last"></div>
|
||||
|
||||
@@ -4,23 +4,7 @@
|
||||
<div
|
||||
id="video_id"
|
||||
class="video closed"
|
||||
data-show-captions="true"
|
||||
data-save-state-url="/save_user_state"
|
||||
data-speed="1.5"
|
||||
data-start=""
|
||||
data-end=""
|
||||
data-saved-video-position="0"
|
||||
data-transcript-language="en"
|
||||
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
|
||||
data-transcript-translation-url="/transcript/translation"
|
||||
data-transcript-available-translations-url="/transcript/available_translations"
|
||||
data-sub="Z5KLxerq05Y"
|
||||
data-sources='["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"]'
|
||||
data-autoplay="False"
|
||||
data-yt-test-timeout="1500"
|
||||
data-yt-api-url="www.youtube.com/iframe_api"
|
||||
data-yt-test-url="gdata.youtube.com/feeds/api/videos/"
|
||||
data-autohide-html5="True"
|
||||
data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/", "source": "", "html5_sources": ["http://youtu.be/3_yD_cEKoCk.mp4"]}'
|
||||
>
|
||||
<div class="focus_grabber first"></div>
|
||||
|
||||
@@ -33,8 +17,6 @@
|
||||
</section>
|
||||
<section class="video-controls is-hidden"></section>
|
||||
</article>
|
||||
|
||||
<ol class="subtitles"><li></li></ol>
|
||||
</div>
|
||||
|
||||
<div class="focus_grabber last"></div>
|
||||
|
||||
@@ -4,22 +4,7 @@
|
||||
<div
|
||||
id="video_id"
|
||||
class="video closed"
|
||||
data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl"
|
||||
data-show-captions="false"
|
||||
data-save-state-url="/save_user_state"
|
||||
data-speed="1.5"
|
||||
data-start=""
|
||||
data-end=""
|
||||
data-saved-video-position="0"
|
||||
data-transcript-language="en"
|
||||
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
|
||||
data-transcript-translation-url="/transcript/translation"
|
||||
data-transcript-available-translations-url="/transcript/available_translations"
|
||||
data-autoplay="False"
|
||||
data-yt-test-timeout="1500"
|
||||
data-yt-api-url="www.youtube.com/iframe_api"
|
||||
data-yt-test-url="gdata.youtube.com/feeds/api/videos/"
|
||||
data-autohide-html5="True"
|
||||
data-metadata='{"streams":"0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl", "showCaptions": false, "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "speed": "1.5", "startTime": "", "end": "", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}'
|
||||
>
|
||||
<div class="focus_grabber first"></div>
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<div class="course-content">
|
||||
<div id="video_example">
|
||||
<div id="example">
|
||||
<div
|
||||
id="video_id"
|
||||
class="video closed"
|
||||
data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}'
|
||||
data-bumper-metadata='{"transcriptLanguage": "en", "showCaptions": "true", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "transcriptTranslationUrl": "/transcript/translation/__lang__/?is_bumper=1", "transcriptAvailableTranslationsUrl": "/transcript/available_translations/?is_bumper=1", "streams": "", "saveStateUrl": "/save_user_state"}'
|
||||
data-poster='{"url": "xmodule/include/fixtures/poster.jpg", "type": "youtube"}'
|
||||
>
|
||||
<div class="focus_grabber first"></div>
|
||||
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
|
||||
<span tabindex="-1" class="btn-play is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
|
||||
<div class="video-player-pre"></div>
|
||||
<section class="video-player">
|
||||
<iframe id="id"></iframe>
|
||||
</section>
|
||||
<div class="video-player-post"></div>
|
||||
<section class="video-controls is-hidden">
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
|
||||
<div class="secondary-controls"></div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="focus_grabber last"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4,22 +4,7 @@
|
||||
<div
|
||||
id="video_id1"
|
||||
class="video closed"
|
||||
data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl"
|
||||
data-show-captions="true"
|
||||
data-save-state-url="/save_user_state"
|
||||
data-speed="1.5"
|
||||
data-start=""
|
||||
data-end=""
|
||||
data-saved-video-position="0"
|
||||
data-transcript-language="en"
|
||||
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
|
||||
data-transcript-translation-url="/transcript/translation"
|
||||
data-transcript-available-translations-url="/transcript/available_translations"
|
||||
data-autoplay="False"
|
||||
data-yt-test-timeout="1500"
|
||||
data-yt-api-url="www.youtube.com/iframe_api"
|
||||
data-yt-test-url="gdata.youtube.com/feeds/api/videos/"
|
||||
data-autohide-html5="True"
|
||||
data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl", "sub": "", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}'
|
||||
>
|
||||
<div class="focus_grabber first"></div>
|
||||
|
||||
@@ -35,35 +20,11 @@
|
||||
<section class="video-controls is-hidden">
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
<li><a class="video_control" href="#" title="Play" role="button" aria-disabled="false"></a></li>
|
||||
<li><div class="vidtime">0:00 / 0:00</div></li>
|
||||
</ul>
|
||||
<div class="secondary-controls">
|
||||
<div class="speeds">
|
||||
<a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false">
|
||||
<span class="label">Speed</span>
|
||||
<span class="value"></span>
|
||||
</a>
|
||||
<ol class="video-speeds"></ol>
|
||||
</div>
|
||||
<div class="volume">
|
||||
<a href="#" title="Volume" role="button" aria-disabled="false"></a>
|
||||
<div class="volume-slider-container">
|
||||
<div class="volume-slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
|
||||
<a href="#" class="quality-control is-hidden" title="HD off" role="button" aria-disabled="false">HD off</a>
|
||||
<div class="lang menu-container">
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
|
||||
<div class="secondary-controls"></div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<ol class="subtitles"><li></li></ol>
|
||||
</div>
|
||||
|
||||
<div class="focus_grabber last"></div>
|
||||
@@ -77,20 +38,7 @@
|
||||
<div
|
||||
id="video_id2"
|
||||
class="video"
|
||||
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
|
||||
data-show-captions="true"
|
||||
data-speed="1.0"
|
||||
data-start=""
|
||||
data-end=""
|
||||
data-transcript-language="en"
|
||||
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
|
||||
data-transcript-translation-url="/transcript/translation"
|
||||
data-transcript-available-translations-url="/transcript/available_translations"
|
||||
data-autoplay="False"
|
||||
data-yt-test-timeout="1500"
|
||||
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
|
||||
|
||||
data-autohide-html5="True"
|
||||
data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.0", "startTime": "", "streams": "0.75:7tqY6eQzVhE,1.0:cogebirgzzM", "sub": "", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}'
|
||||
>
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
@@ -102,30 +50,8 @@
|
||||
<section class="video-controls">
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
<li><a class="video_control" href="#" title="Play"></a></li>
|
||||
<li><div class="vidtime">0:00 / 0:00</div></li>
|
||||
</ul>
|
||||
<div class="secondary-controls">
|
||||
<div class="speeds">
|
||||
<a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false">
|
||||
<span class="label">Speed</span>
|
||||
<span class="value"></span>
|
||||
</a>
|
||||
<ol class="video-speeds"></ol>
|
||||
</div>
|
||||
<div class="volume">
|
||||
<a href="#"></a>
|
||||
<div class="volume-slider-container">
|
||||
<div class="volume-slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
|
||||
<a href="#" class="quality-control is-hidden" title="HD">HD</a>
|
||||
<div class="lang menu-container">
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
|
||||
<div class="secondary-controls"></div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
@@ -142,20 +68,7 @@
|
||||
<div
|
||||
id="video_id3"
|
||||
class="video"
|
||||
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
|
||||
data-show-captions="true"
|
||||
data-speed="1.0"
|
||||
data-start=""
|
||||
data-end=""
|
||||
data-transcript-language="en"
|
||||
data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
|
||||
data-transcript-translation-url="/transcript/translation"
|
||||
data-transcript-available-translations-url="/transcript/available_translations"
|
||||
data-autoplay="False"
|
||||
data-yt-test-timeout="1500"
|
||||
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
|
||||
|
||||
data-autohide-html5="True"
|
||||
data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.0", "startTime": "", "streams": "0.75:7tqY6eQzVhE,1.0:cogebirgzzM", "sub": "", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}'
|
||||
>
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
|
||||
@@ -206,6 +206,9 @@
|
||||
},
|
||||
toBeInArray: function (array) {
|
||||
return $.inArray(this.actual, array) > -1;
|
||||
},
|
||||
toBeFocused: function () {
|
||||
return $(this.actual)[0] === $(this.actual)[0].ownerDocument.activeElement;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -239,12 +242,11 @@
|
||||
loadFixtures('video_all.html');
|
||||
}
|
||||
|
||||
// If `params` is an object, assign it's properties as data attributes
|
||||
// If `params` is an object, assign its properties as data attributes
|
||||
// to the main video DIV element.
|
||||
if (_.isObject(params)) {
|
||||
$('#example')
|
||||
.find('#video_id')
|
||||
.data(params);
|
||||
var metadata = _.extend($('#video_id').data('metadata'), params);
|
||||
$('#video_id').data('metadata', metadata);
|
||||
}
|
||||
|
||||
jasmine.stubRequests();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
(function (undefined) {
|
||||
describe('Video', function () {
|
||||
var oldOTBD;
|
||||
var oldOTBD, state;
|
||||
|
||||
beforeEach(function () {
|
||||
jasmine.stubRequests();
|
||||
@@ -17,11 +17,12 @@
|
||||
beforeEach(function () {
|
||||
loadFixtures('video.html');
|
||||
$.cookie.andReturn('0.50');
|
||||
this.state = jasmine.initializePlayerYouTube('video_html5.html');
|
||||
});
|
||||
|
||||
describe('by default', function () {
|
||||
beforeEach(function () {
|
||||
this.state = new window.Video('#example');
|
||||
afterEach(function () {
|
||||
this.state.videoPlayer.destroy();
|
||||
});
|
||||
|
||||
it('check videoType', function () {
|
||||
@@ -54,19 +55,16 @@
|
||||
var state;
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('video_html5.html');
|
||||
$.cookie.andReturn('0.75');
|
||||
state = jasmine.initializePlayer('video_html5.html');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
state.videoPlayer.destroy();
|
||||
state = undefined;
|
||||
});
|
||||
|
||||
describe('by default', function () {
|
||||
beforeEach(function () {
|
||||
state = new window.Video('#example');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
state = undefined;
|
||||
});
|
||||
|
||||
it('check videoType', function () {
|
||||
expect(state.videoType).toEqual('html5');
|
||||
});
|
||||
@@ -95,14 +93,6 @@
|
||||
// the stand alone HTML5 player object is already loaded, so no
|
||||
// further testing in that case is required.
|
||||
describe('HTML5 API is available', function () {
|
||||
beforeEach(function () {
|
||||
state = new Video('#example');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
state = null;
|
||||
});
|
||||
|
||||
it('create the Video Player', function () {
|
||||
expect(state.videoPlayer.player).not.toBeUndefined();
|
||||
});
|
||||
@@ -113,8 +103,11 @@
|
||||
describe('YouTube API is not loaded', function () {
|
||||
beforeEach(function () {
|
||||
window.YT = undefined;
|
||||
state = jasmine.initializePlayerYouTube();
|
||||
})
|
||||
|
||||
state = jasmine.initializePlayerYouTube('video.html');
|
||||
afterEach(function () {
|
||||
state.videoPlayer.destroy();
|
||||
});
|
||||
|
||||
it('callback, to be called after YouTube API loads, exists and is called', function () {
|
||||
@@ -159,9 +152,8 @@
|
||||
}
|
||||
];
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('video.html');
|
||||
|
||||
afterEach(function () {
|
||||
state.videoPlayer.destroy();
|
||||
});
|
||||
|
||||
$.each(miniTestSuite, function (index, test) {
|
||||
@@ -172,13 +164,10 @@
|
||||
|
||||
function itFabrique(itDescription, data, expectData) {
|
||||
it(itDescription, function () {
|
||||
$('#example').find('.video')
|
||||
.data({
|
||||
'start': data.start,
|
||||
'end': data.end
|
||||
});
|
||||
|
||||
state = new Video('#example');
|
||||
state = jasmine.initializePlayer('video.html', {
|
||||
'start': data.start,
|
||||
'end': data.end
|
||||
});
|
||||
|
||||
expect(state.config.startTime).toBe(expectData.start);
|
||||
expect(state.config.endTime).toBe(expectData.end);
|
||||
@@ -238,26 +227,5 @@
|
||||
expect(numAjaxCalls).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('log', function () {
|
||||
beforeEach(function () {
|
||||
loadFixtures('video_html5.html');
|
||||
state = new Video('#example');
|
||||
spyOn(Logger, 'log');
|
||||
state.videoPlayer.log('someEvent', {
|
||||
currentTime: 25,
|
||||
speed: '1.0'
|
||||
});
|
||||
});
|
||||
|
||||
it('call the logger with valid extra parameters', function () {
|
||||
expect(Logger.log).toHaveBeenCalledWith('someEvent', {
|
||||
id: 'id',
|
||||
code: 'html5',
|
||||
currentTime: 25,
|
||||
speed: '1.0'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}).call(this);
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
afterEach(function () {
|
||||
state.storage.clear();
|
||||
state.videoPlayer.destroy();
|
||||
$.fn.scrollTo.reset();
|
||||
$('.subtitles').remove();
|
||||
$('source').remove();
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
});
|
||||
|
||||
@@ -12,158 +12,6 @@ function (Initialize) {
|
||||
state = {};
|
||||
});
|
||||
|
||||
describe('saveState function', function () {
|
||||
var videoPlayerCurrentTime, newCurrentTime, speed;
|
||||
|
||||
// We make sure that `currentTime` is a float. We need to test
|
||||
// that Math.round() is called.
|
||||
videoPlayerCurrentTime = 3.1242;
|
||||
|
||||
// We have two times, because one is stored in
|
||||
// `videoPlayer.currentTime`, and the other is passed directly to
|
||||
// `saveState` in `data` object. In each case, there is different
|
||||
// code that handles these times. They have to be different for
|
||||
// test completeness sake. Also, make sure it is float, as is the
|
||||
// time above.
|
||||
newCurrentTime = 5.4;
|
||||
|
||||
speed = '0.75';
|
||||
|
||||
beforeEach(function () {
|
||||
state = {
|
||||
videoPlayer: {
|
||||
currentTime: videoPlayerCurrentTime
|
||||
},
|
||||
storage: {
|
||||
setItem: jasmine.createSpy()
|
||||
},
|
||||
config: {
|
||||
saveStateUrl: 'http://example.com/save_user_state'
|
||||
}
|
||||
};
|
||||
|
||||
spyOn($, 'ajax');
|
||||
spyOn(Time, 'formatFull').andCallThrough();
|
||||
});
|
||||
|
||||
it('data is not an object, async is true', function () {
|
||||
itSpec({
|
||||
asyncVal: true,
|
||||
speedVal: undefined,
|
||||
positionVal: videoPlayerCurrentTime,
|
||||
data: undefined,
|
||||
ajaxData: {
|
||||
saved_video_position: Time.formatFull(Math.round(videoPlayerCurrentTime))
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('data contains speed, async is false', function () {
|
||||
itSpec({
|
||||
asyncVal: false,
|
||||
speedVal: speed,
|
||||
positionVal: undefined,
|
||||
data: {
|
||||
speed: speed
|
||||
},
|
||||
ajaxData: {
|
||||
speed: speed
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('data contains float position, async is true', function () {
|
||||
itSpec({
|
||||
asyncVal: true,
|
||||
speedVal: undefined,
|
||||
positionVal: newCurrentTime,
|
||||
data: {
|
||||
saved_video_position: newCurrentTime
|
||||
},
|
||||
ajaxData: {
|
||||
saved_video_position: Time.formatFull(Math.round(newCurrentTime))
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('data contains speed and rounded position, async is false', function () {
|
||||
itSpec({
|
||||
asyncVal: false,
|
||||
speedVal: speed,
|
||||
positionVal: Math.round(newCurrentTime),
|
||||
data: {
|
||||
speed: speed,
|
||||
saved_video_position: Math.round(newCurrentTime)
|
||||
},
|
||||
ajaxData: {
|
||||
speed: speed,
|
||||
saved_video_position: Time.formatFull(Math.round(newCurrentTime))
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('data contains empty object, async is true', function () {
|
||||
itSpec({
|
||||
asyncVal: true,
|
||||
speedVal: undefined,
|
||||
positionVal: undefined,
|
||||
data: {},
|
||||
ajaxData: {}
|
||||
});
|
||||
});
|
||||
|
||||
it('data contains position 0, async is true', function () {
|
||||
itSpec({
|
||||
asyncVal: true,
|
||||
speedVal: undefined,
|
||||
positionVal: 0,
|
||||
data: {
|
||||
saved_video_position: 0
|
||||
},
|
||||
ajaxData: {
|
||||
saved_video_position: Time.formatFull(Math.round(0))
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
function itSpec(value) {
|
||||
var asyncVal = value.asyncVal,
|
||||
speedVal = value.speedVal,
|
||||
positionVal = value.positionVal,
|
||||
data = value.data,
|
||||
ajaxData = value.ajaxData;
|
||||
|
||||
Initialize.prototype.saveState.call(state, asyncVal, data);
|
||||
|
||||
if (speedVal) {
|
||||
expect(state.storage.setItem).toHaveBeenCalledWith(
|
||||
'speed',
|
||||
speedVal,
|
||||
true
|
||||
);
|
||||
}
|
||||
if (positionVal) {
|
||||
expect(state.storage.setItem).toHaveBeenCalledWith(
|
||||
'savedVideoPosition',
|
||||
positionVal,
|
||||
true
|
||||
);
|
||||
expect(Time.formatFull).toHaveBeenCalledWith(
|
||||
positionVal
|
||||
);
|
||||
}
|
||||
expect($.ajax).toHaveBeenCalledWith({
|
||||
url: state.config.saveStateUrl,
|
||||
type: 'POST',
|
||||
async: asyncVal,
|
||||
dataType: 'json',
|
||||
data: ajaxData
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('getCurrentLanguage', function () {
|
||||
var msg;
|
||||
|
||||
@@ -356,20 +204,12 @@ function (Initialize) {
|
||||
|
||||
describe('when new speed is available', function () {
|
||||
beforeEach(function () {
|
||||
Initialize.prototype.setSpeed.call(state, '0.75', true);
|
||||
Initialize.prototype.setSpeed.call(state, '0.75');
|
||||
});
|
||||
|
||||
it('set new speed', function () {
|
||||
expect(state.speed).toEqual('0.75');
|
||||
});
|
||||
|
||||
it('save setting for new speed', function () {
|
||||
expect(state.storage.setItem.calls[0].args)
|
||||
.toEqual(['speed', '0.75', true]);
|
||||
|
||||
expect(state.storage.setItem.calls[1].args)
|
||||
.toEqual(['general_speed', '0.75']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when new speed is not available', function () {
|
||||
@@ -390,7 +230,7 @@ function (Initialize) {
|
||||
};
|
||||
|
||||
$.each(map, function(key, expected) {
|
||||
Initialize.prototype.setSpeed.call(state, key, true);
|
||||
Initialize.prototype.setSpeed.call(state, key);
|
||||
expect(state.speed).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
afterEach(function () {
|
||||
$('source').remove();
|
||||
state.storage.clear();
|
||||
state.videoPlayer.destroy();
|
||||
});
|
||||
|
||||
describe('constructor', function () {
|
||||
@@ -56,24 +57,6 @@
|
||||
});
|
||||
*/
|
||||
});
|
||||
|
||||
it('add ARIA attributes to button, menu, and menu items links',
|
||||
function () {
|
||||
expect(button).toHaveAttrs({
|
||||
'role': 'button',
|
||||
'title': '.srt',
|
||||
'aria-disabled': 'false'
|
||||
});
|
||||
|
||||
expect(menuList).toHaveAttr('role', 'menu');
|
||||
|
||||
menuItemsLinks.each(function(){
|
||||
expect($(this)).toHaveAttrs({
|
||||
'role': 'menuitem',
|
||||
'aria-disabled': 'false'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when running', function () {
|
||||
|
||||
109
common/lib/xmodule/xmodule/js/spec/video/video_bumper_spec.js
Normal file
109
common/lib/xmodule/xmodule/js/spec/video/video_bumper_spec.js
Normal file
@@ -0,0 +1,109 @@
|
||||
(function (WAIT_TIMEOUT) {
|
||||
'use strict';
|
||||
describe('VideoBumper', function () {
|
||||
var state, oldOTBD, waitForPlaying;
|
||||
|
||||
waitForPlaying = function (state) {
|
||||
waitsFor(function () {
|
||||
return state.el.hasClass('is-playing');
|
||||
}, 'Player is not playing.', WAIT_TIMEOUT);
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine
|
||||
.createSpy('onTouchBasedDevice').andReturn(null);
|
||||
state = jasmine.initializePlayer('video_with_bumper.html');
|
||||
$('.poster .btn-play').click();
|
||||
jasmine.Clock.useMock();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
$('source').remove();
|
||||
state.storage.clear();
|
||||
if (state.bumperState && state.bumperState.videoPlayer) {
|
||||
state.bumperState.videoPlayer.destroy();
|
||||
}
|
||||
if (state.videoPlayer) {
|
||||
state.videoPlayer.destroy();
|
||||
}
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
});
|
||||
|
||||
it('can render the bumper video', function () {
|
||||
expect($('.is-bumper')).toExist();
|
||||
});
|
||||
|
||||
it('can show the main video on error', function () {
|
||||
state.el.trigger('error');
|
||||
jasmine.Clock.tick(20);
|
||||
expect($('.is-bumper')).not.toExist();
|
||||
waitForPlaying(state);
|
||||
});
|
||||
|
||||
it('can show the main video once bumper ends', function () {
|
||||
state.el.trigger('ended');
|
||||
jasmine.Clock.tick(20);
|
||||
expect($('.is-bumper')).not.toExist();
|
||||
waitForPlaying(state);
|
||||
});
|
||||
|
||||
it('can show the main video on skip', function () {
|
||||
state.bumperState.videoBumper.skip();
|
||||
jasmine.Clock.tick(20);
|
||||
expect($('.is-bumper')).not.toExist();
|
||||
waitForPlaying(state);
|
||||
});
|
||||
|
||||
it('can stop the bumper video playing if it is too long', function () {
|
||||
state.el.trigger('timeupdate', [state.bumperState.videoBumper.maxBumperDuration + 1]);
|
||||
jasmine.Clock.tick(20);
|
||||
expect($('.is-bumper')).not.toExist();
|
||||
waitForPlaying(state);
|
||||
});
|
||||
|
||||
it('can save appropriate states correctly on ended', function () {
|
||||
var saveState = jasmine.createSpy('saveState');
|
||||
state.bumperState.videoSaveStatePlugin.saveState = saveState;
|
||||
state.el.trigger('ended');
|
||||
jasmine.Clock.tick(20);
|
||||
expect(saveState).toHaveBeenCalledWith(true, {
|
||||
bumper_last_view_date: true});
|
||||
});
|
||||
|
||||
it('can save appropriate states correctly on skip', function () {
|
||||
var saveState = jasmine.createSpy('saveState');
|
||||
state.bumperState.videoSaveStatePlugin.saveState = saveState;
|
||||
state.bumperState.videoBumper.skip();
|
||||
expect(state.storage.getItem('isBumperShown')).toBeTruthy();
|
||||
jasmine.Clock.tick(20);
|
||||
expect(saveState).toHaveBeenCalledWith(true, {
|
||||
bumper_last_view_date: true});
|
||||
});
|
||||
|
||||
it('can save appropriate states correctly on error', function () {
|
||||
var saveState = jasmine.createSpy('saveState');
|
||||
state.bumperState.videoSaveStatePlugin.saveState = saveState;
|
||||
state.el.trigger('error');
|
||||
expect(state.storage.getItem('isBumperShown')).toBeTruthy();
|
||||
jasmine.Clock.tick(20);
|
||||
expect(saveState).toHaveBeenCalledWith(true, {
|
||||
bumper_last_view_date: true});
|
||||
});
|
||||
|
||||
it('can save appropriate states correctly on skip and do not show again', function () {
|
||||
var saveState = jasmine.createSpy('saveState');
|
||||
state.bumperState.videoSaveStatePlugin.saveState = saveState;
|
||||
state.bumperState.videoBumper.skipAndDoNotShowAgain();
|
||||
expect(state.storage.getItem('isBumperShown')).toBeTruthy();
|
||||
jasmine.Clock.tick(20);
|
||||
expect(saveState).toHaveBeenCalledWith(true, {
|
||||
bumper_last_view_date: true, bumper_do_not_show_again: true});
|
||||
});
|
||||
|
||||
it('can destroy itself', function () {
|
||||
state.bumperState.videoBumper.destroy();
|
||||
expect(state.videoBumper).toBeUndefined();
|
||||
});
|
||||
});
|
||||
}).call(this, window.WAIT_TIMEOUT);
|
||||
@@ -11,14 +11,13 @@
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
$('.subtitles').remove();
|
||||
|
||||
// `source` tags should be removed to avoid memory leak bug that we
|
||||
// had before. Removing of `source` tag, not `video` tag, stops
|
||||
// loading video source and clears the memory.
|
||||
$('source').remove();
|
||||
$.fn.scrollTo.reset();
|
||||
state.storage.clear();
|
||||
state.videoPlayer.destroy();
|
||||
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
});
|
||||
@@ -121,11 +120,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
it('bind the hide caption button', function () {
|
||||
state = jasmine.initializePlayer();
|
||||
expect($('.hide-subtitles')).toHandle('click');
|
||||
});
|
||||
|
||||
it('bind the mouse movement', function () {
|
||||
state = jasmine.initializePlayer();
|
||||
expect($('.subtitles')).toHandle('mouseover');
|
||||
@@ -143,6 +137,27 @@
|
||||
|
||||
});
|
||||
|
||||
it('can destroy itself', function () {
|
||||
spyOn($, 'ajaxWithPrefix');
|
||||
state = jasmine.initializePlayer();
|
||||
var plugin = state.videoCaption;
|
||||
|
||||
spyOn($.fn, 'off').andCallThrough();
|
||||
state.videoCaption.destroy();
|
||||
|
||||
expect(state.videoCaption).toBeUndefined();
|
||||
expect($.fn.off).toHaveBeenCalledWith({
|
||||
'caption:fetch': plugin.fetchCaption,
|
||||
'caption:resize': plugin.onResize,
|
||||
'caption:update': plugin.onCaptionUpdate,
|
||||
'ended': plugin.pause,
|
||||
'fullscreen': plugin.onResize,
|
||||
'pause': plugin.pause,
|
||||
'play': plugin.play,
|
||||
'destroy': plugin.destroy
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderLanguageMenu', function () {
|
||||
describe('is rendered', function () {
|
||||
it('if languages more than 1', function () {
|
||||
@@ -593,7 +608,7 @@
|
||||
it(msg, function () {
|
||||
spyOn(Caption, 'fetchAvailableTranslations');
|
||||
$.ajax.andCallFake(function (settings) {
|
||||
settings.error([]);
|
||||
_.result(settings, 'error');
|
||||
});
|
||||
|
||||
state.config.transcriptLanguages = {};
|
||||
@@ -612,7 +627,7 @@
|
||||
xit(msg, function () {
|
||||
$.ajax
|
||||
.andCallFake(function (settings) {
|
||||
settings.error([]);
|
||||
_.result(settings, 'error');
|
||||
});
|
||||
|
||||
state.config.transcriptLanguages = {
|
||||
@@ -690,7 +705,7 @@
|
||||
msg = 'on error: captions are hidden if there are no transcript';
|
||||
it(msg, function () {
|
||||
$.ajax.andCallFake(function (settings) {
|
||||
settings.error();
|
||||
_.result(settings, 'error');
|
||||
});
|
||||
Caption.fetchAvailableTranslations();
|
||||
|
||||
@@ -907,8 +922,8 @@
|
||||
$('.subtitles').css('maxHeight'), 10
|
||||
);
|
||||
videoWrapperHeight = $('.video-wrapper').height();
|
||||
progressSliderHeight = videoControl.sliderEl.height();
|
||||
controlHeight = videoControl.el.height();
|
||||
progressSliderHeight = state.el.find('.slider').height();
|
||||
controlHeight = state.el.find('.video-controls').height();
|
||||
shouldBeHeight = videoWrapperHeight -
|
||||
0.5 * progressSliderHeight -
|
||||
controlHeight;
|
||||
@@ -1043,7 +1058,6 @@
|
||||
describe('toggle', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
spyOn(state.videoPlayer, 'log');
|
||||
$('.subtitles li[data-index=1]').addClass('current');
|
||||
});
|
||||
|
||||
@@ -1053,15 +1067,6 @@
|
||||
state.videoCaption.toggle(jQuery.Event('click'));
|
||||
});
|
||||
|
||||
it('log the hide_transcript event', function () {
|
||||
expect(state.videoPlayer.log).toHaveBeenCalledWith(
|
||||
'hide_transcript',
|
||||
{
|
||||
currentTime: state.videoPlayer.currentTime
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('hide the caption', function () {
|
||||
expect(state.el).toHaveClass('closed');
|
||||
});
|
||||
@@ -1079,15 +1084,6 @@
|
||||
jasmine.Clock.useMock();
|
||||
});
|
||||
|
||||
it('log the show_transcript event', function () {
|
||||
expect(state.videoPlayer.log).toHaveBeenCalledWith(
|
||||
'show_transcript',
|
||||
{
|
||||
currentTime: state.videoPlayer.currentTime
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('show the caption', function () {
|
||||
expect(state.el).not.toHaveClass('closed');
|
||||
});
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
$('source').remove();
|
||||
_.result(state.storage, 'clear');
|
||||
_.result($('video').data('contextmenu'), 'destroy');
|
||||
_.result(state.videoPlayer, 'destroy');
|
||||
});
|
||||
|
||||
describe('constructor', function () {
|
||||
@@ -219,12 +220,13 @@
|
||||
|
||||
it('mouse left/right-clicking behaves as expected on play/pause menu item', function () {
|
||||
var menuItem = menuItems.first();
|
||||
spyOn(state.videoPlayer, 'isPlaying');
|
||||
spyOn(state.videoPlayer, 'play').andCallFake(function () {
|
||||
state.videoControl.isPlaying = true;
|
||||
state.videoPlayer.isPlaying.andReturn(true);
|
||||
state.el.trigger('play');
|
||||
});
|
||||
spyOn(state.videoPlayer, 'pause').andCallFake(function () {
|
||||
state.videoControl.isPlaying = false;
|
||||
state.videoPlayer.isPlaying.andReturn(false);
|
||||
state.el.trigger('pause');
|
||||
});
|
||||
// Left-click on play
|
||||
|
||||
@@ -13,12 +13,13 @@
|
||||
afterEach(function () {
|
||||
$('source').remove();
|
||||
state.storage.clear();
|
||||
window.Video.previousState = null;
|
||||
state.videoPlayer.destroy();
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
});
|
||||
|
||||
describe('constructor', function () {
|
||||
beforeEach(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer();
|
||||
});
|
||||
|
||||
@@ -28,83 +29,13 @@
|
||||
'.slider',
|
||||
'ul.vcr',
|
||||
'a.play',
|
||||
'.vidtime',
|
||||
'.add-fullscreen'
|
||||
'.vidtime'
|
||||
].join(',')
|
||||
);
|
||||
|
||||
expect($('.video-controls').find('.vidtime'))
|
||||
.toHaveText('0:00 / 0:00');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
it('add ARIA attributes to play control', function () {
|
||||
var playControl = $('ul.vcr a');
|
||||
|
||||
expect(playControl).toHaveAttrs({
|
||||
'role': 'button',
|
||||
'title': 'Play',
|
||||
'aria-disabled': 'false'
|
||||
});
|
||||
});
|
||||
|
||||
it('add ARIA attributes to fullscreen control', function () {
|
||||
var fullScreenControl = $('a.add-fullscreen');
|
||||
|
||||
expect(fullScreenControl).toHaveAttrs({
|
||||
'role': 'button',
|
||||
'title': 'Fill browser',
|
||||
'aria-disabled': 'false'
|
||||
});
|
||||
});
|
||||
|
||||
it('bind the playback button', function () {
|
||||
expect($('.video_control')).toHandleWith(
|
||||
'click',
|
||||
state.videoControl.togglePlayback
|
||||
);
|
||||
});
|
||||
|
||||
describe('when on a non-touch based device', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
});
|
||||
|
||||
it('add the play class to video control', function () {
|
||||
expect($('.video_control')).toHaveClass('play');
|
||||
expect($('.video_control')).toHaveAttr(
|
||||
'title', 'Play'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when on a touch based device', function () {
|
||||
beforeEach(function () {
|
||||
window.onTouchBasedDevice.andReturn(['iPad']);
|
||||
state = jasmine.initializePlayer();
|
||||
});
|
||||
|
||||
it(
|
||||
'does not add the play class to video control',
|
||||
function ()
|
||||
{
|
||||
expect($('.video_control')).toHaveClass('play');
|
||||
expect($('.video_control')).toHaveAttr(
|
||||
'title', 'Play'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('constructor with start-time', function () {
|
||||
@@ -115,6 +46,7 @@
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
start: 10,
|
||||
savedVideoPosition: 0
|
||||
@@ -147,6 +79,7 @@
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
start: 10,
|
||||
savedVideoPosition: 15
|
||||
@@ -181,6 +114,7 @@
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
start: 10,
|
||||
savedVideoPosition: -15
|
||||
@@ -215,6 +149,7 @@
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
start: 10,
|
||||
savedVideoPosition: 'a'
|
||||
@@ -249,6 +184,7 @@
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
start: 10,
|
||||
savedVideoPosition: 10000
|
||||
@@ -278,13 +214,14 @@
|
||||
|
||||
describe('constructor with end-time', function () {
|
||||
it(
|
||||
'saved position is 0, timer slider and VCR set to 0:00 ' +
|
||||
'saved position is 0, timer slider and VCR set to 0:00 ' +
|
||||
'and ending at specified end-time',
|
||||
function ()
|
||||
{
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
end: 20,
|
||||
savedVideoPosition: 0
|
||||
@@ -319,6 +256,7 @@
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
end: 20,
|
||||
savedVideoPosition: 15
|
||||
@@ -353,6 +291,7 @@
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
end: 20,
|
||||
savedVideoPosition: -15
|
||||
@@ -387,6 +326,7 @@
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
end: 20,
|
||||
savedVideoPosition: 'a'
|
||||
@@ -422,6 +362,7 @@
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
end: 20,
|
||||
savedVideoPosition: 10000
|
||||
@@ -457,6 +398,7 @@
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
start: 10,
|
||||
end: 20,
|
||||
@@ -492,6 +434,7 @@
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
start: 10,
|
||||
end: 20,
|
||||
@@ -527,6 +470,7 @@
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
start: 10,
|
||||
end: 20,
|
||||
@@ -562,6 +506,7 @@
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
start: 10,
|
||||
end: 20,
|
||||
@@ -597,6 +542,7 @@
|
||||
var duration, sliderEl, expectedValue;
|
||||
|
||||
runs(function () {
|
||||
window.VideoState = {};
|
||||
state = jasmine.initializePlayer({
|
||||
start: 10,
|
||||
end: 20,
|
||||
@@ -625,217 +571,8 @@
|
||||
});
|
||||
});
|
||||
|
||||
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.videoControl.height).toBe(150);
|
||||
});
|
||||
|
||||
describe('play', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
state.videoControl.play();
|
||||
});
|
||||
|
||||
it('switch playback button to play state', function () {
|
||||
expect($('.video_control')).not.toHaveClass('play');
|
||||
expect($('.video_control')).toHaveClass('pause');
|
||||
expect($('.video_control')).toHaveAttr('title', 'Pause');
|
||||
});
|
||||
});
|
||||
|
||||
describe('pause', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
state.videoControl.pause();
|
||||
});
|
||||
|
||||
it('switch playback button to pause state', function () {
|
||||
expect($('.video_control')).not.toHaveClass('pause');
|
||||
expect($('.video_control')).toHaveClass('play');
|
||||
expect($('.video_control')).toHaveAttr('title', 'Play');
|
||||
});
|
||||
});
|
||||
|
||||
describe('togglePlayback', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
});
|
||||
|
||||
describe(
|
||||
'when the control does not have play or pause class',
|
||||
function ()
|
||||
{
|
||||
beforeEach(function () {
|
||||
$('.video_control').removeClass('play')
|
||||
.removeClass('pause');
|
||||
});
|
||||
|
||||
describe('when the video is playing', function () {
|
||||
beforeEach(function () {
|
||||
$('.video_control').addClass('play');
|
||||
spyOnEvent(state.videoControl, 'pause');
|
||||
state.videoControl.togglePlayback(
|
||||
$.Event('click')
|
||||
);
|
||||
});
|
||||
|
||||
it('does not trigger the pause event', function () {
|
||||
expect('pause').not
|
||||
.toHaveBeenTriggeredOn(state.videoControl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the video is paused', function () {
|
||||
beforeEach(function () {
|
||||
$('.video_control').addClass('pause');
|
||||
spyOnEvent(state.videoControl, 'play');
|
||||
state.videoControl.togglePlayback(
|
||||
$.Event('click')
|
||||
);
|
||||
});
|
||||
|
||||
it('does not trigger the play event', function () {
|
||||
expect('play').not
|
||||
.toHaveBeenTriggeredOn(state.videoControl);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Play placeholder', function () {
|
||||
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.videoControl.showPlayPlaceholder();
|
||||
|
||||
expect(btnPlay).not.toHaveClass('is-hidden');
|
||||
expect(btnPlay).toHaveAttrs({
|
||||
'aria-hidden': 'false',
|
||||
'tabindex': 0
|
||||
});
|
||||
|
||||
state.videoControl.hidePlayPlaceholder();
|
||||
|
||||
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.videoControl.play();
|
||||
state.videoControl.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.videoControl.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.videoControl.play();
|
||||
state.videoControl.pause();
|
||||
|
||||
expect(btnPlay).toHaveClass('is-hidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('show', function () {
|
||||
var controls;
|
||||
|
||||
state = jasmine.initializePlayer();
|
||||
controls = state.el.find('.video-controls');
|
||||
controls.addClass('is-hidden');
|
||||
@@ -843,5 +580,23 @@
|
||||
state.videoControl.show();
|
||||
expect(controls).not.toHaveClass('is-hidden');
|
||||
});
|
||||
|
||||
it('can destroy itself', function () {
|
||||
state = jasmine.initializePlayer();
|
||||
state.videoControl.destroy();
|
||||
expect(state.videoControl).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can focus the first control', function () {
|
||||
var btnPlay;
|
||||
state = jasmine.initializePlayer({focusFirstControl: true});
|
||||
btnPlay = state.el.find('.video-controls .play');
|
||||
waitsFor(function () {
|
||||
return state.el.hasClass('is-initialized');
|
||||
}, 'Player is not initialized', WAIT_TIMEOUT);
|
||||
runs(function () {
|
||||
expect(btnPlay).toBeFocused();
|
||||
});
|
||||
});
|
||||
});
|
||||
}).call(this, window.WAIT_TIMEOUT);
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
(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);
|
||||
@@ -0,0 +1,166 @@
|
||||
(function (undefined) {
|
||||
'use strict';
|
||||
describe('VideoPlayer Events plugin', function () {
|
||||
var state, oldOTBD;
|
||||
|
||||
beforeEach(function () {
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine
|
||||
.createSpy('onTouchBasedDevice')
|
||||
.andReturn(null);
|
||||
|
||||
jasmine.stubRequests();
|
||||
state = jasmine.initializePlayer();
|
||||
spyOn(Logger, 'log');
|
||||
spyOn(state.videoEventsPlugin, 'getCurrentTime').andReturn(10);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
$('source').remove();
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
state.storage.clear();
|
||||
if (state.videoPlayer) {
|
||||
state.videoPlayer.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it('can emit "load_video" event', function () {
|
||||
state.el.trigger('ready');
|
||||
expect(Logger.log).toHaveBeenCalledWith('load_video', {
|
||||
id: 'id',
|
||||
code: 'html5'
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "play_video" event', function () {
|
||||
state.el.trigger('play');
|
||||
expect(Logger.log).toHaveBeenCalledWith('play_video', {
|
||||
id: 'id',
|
||||
code: 'html5',
|
||||
currentTime: 10
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "pause_video" event', function () {
|
||||
state.el.trigger('pause');
|
||||
expect(Logger.log).toHaveBeenCalledWith('pause_video', {
|
||||
id: 'id',
|
||||
code: 'html5',
|
||||
currentTime: 10
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "speed_change_video" event', function () {
|
||||
state.el.trigger('speedchange', ['2.0', '1.0']);
|
||||
expect(Logger.log).toHaveBeenCalledWith('speed_change_video', {
|
||||
id: 'id',
|
||||
code: 'html5',
|
||||
current_time: 10,
|
||||
old_speed: '1.0',
|
||||
new_speed: '2.0'
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "seek_video" event', function () {
|
||||
state.el.trigger('seek', [1, 0, 'any']);
|
||||
expect(Logger.log).toHaveBeenCalledWith('seek_video', {
|
||||
id: 'id',
|
||||
code: 'html5',
|
||||
old_time: 0,
|
||||
new_time: 1,
|
||||
type: 'any'
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "stop_video" event', function () {
|
||||
state.el.trigger('ended');
|
||||
expect(Logger.log).toHaveBeenCalledWith('stop_video', {
|
||||
id: 'id',
|
||||
code: 'html5',
|
||||
currentTime: 10
|
||||
});
|
||||
|
||||
Logger.log.reset();
|
||||
state.el.trigger('stop');
|
||||
expect(Logger.log).toHaveBeenCalledWith('stop_video', {
|
||||
id: 'id',
|
||||
code: 'html5',
|
||||
currentTime: 10
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "skip_video" event', function () {
|
||||
state.el.trigger('skip', [false]);
|
||||
expect(Logger.log).toHaveBeenCalledWith('skip_video', {
|
||||
id: 'id',
|
||||
code: 'html5',
|
||||
currentTime: 10
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "do_not_show_again_video" event', function () {
|
||||
state.el.trigger('skip', [true]);
|
||||
expect(Logger.log).toHaveBeenCalledWith('do_not_show_again_video', {
|
||||
id: 'id',
|
||||
code: 'html5',
|
||||
currentTime: 10
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "video_show_cc_menu" event', function () {
|
||||
state.el.trigger('language_menu:show');
|
||||
expect(Logger.log).toHaveBeenCalledWith('video_show_cc_menu', {
|
||||
id: 'id',
|
||||
code: 'html5'
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "video_hide_cc_menu" event', function () {
|
||||
state.el.trigger('language_menu:hide');
|
||||
expect(Logger.log).toHaveBeenCalledWith('video_hide_cc_menu', {
|
||||
id: 'id',
|
||||
code: 'html5'
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "show_transcript" event', function () {
|
||||
state.el.trigger('captions:show');
|
||||
expect(Logger.log).toHaveBeenCalledWith('show_transcript', {
|
||||
id: 'id',
|
||||
code: 'html5',
|
||||
current_time: 10
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "hide_transcript" event', function () {
|
||||
state.el.trigger('captions:hide');
|
||||
expect(Logger.log).toHaveBeenCalledWith('hide_transcript', {
|
||||
id: 'id',
|
||||
code: 'html5',
|
||||
current_time: 10
|
||||
});
|
||||
});
|
||||
|
||||
it('can destroy itself', function () {
|
||||
var plugin = state.videoEventsPlugin;
|
||||
spyOn($.fn, 'off').andCallThrough();
|
||||
state.videoEventsPlugin.destroy();
|
||||
expect(state.videoEventsPlugin).toBeUndefined();
|
||||
expect($.fn.off).toHaveBeenCalledWith({
|
||||
'ready': plugin.onReady,
|
||||
'play': plugin.onPlay,
|
||||
'pause': plugin.onPause,
|
||||
'ended stop': plugin.onEnded,
|
||||
'seek': plugin.onSeek,
|
||||
'skip': plugin.onSkip,
|
||||
'speedchange': plugin.onSpeedChange,
|
||||
'language_menu:show': plugin.onShowLanguageMenu,
|
||||
'language_menu:hide': plugin.onHideLanguageMenu,
|
||||
'captions:show': plugin.onShowCaptions,
|
||||
'captions:hide': plugin.onHideCaptions,
|
||||
'destroy': plugin.destroy
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -26,6 +26,7 @@
|
||||
afterEach(function () {
|
||||
// Turn jQuery animations back on.
|
||||
jQuery.fx.off = true;
|
||||
state.videoPlayer.destroy();
|
||||
});
|
||||
|
||||
it(
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
(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);
|
||||
@@ -0,0 +1,68 @@
|
||||
(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);
|
||||
@@ -0,0 +1,151 @@
|
||||
(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);
|
||||
@@ -0,0 +1,64 @@
|
||||
(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);
|
||||
@@ -1,5 +1,4 @@
|
||||
(function (requirejs, require, define, undefined) {
|
||||
|
||||
'use strict';
|
||||
|
||||
require(
|
||||
@@ -21,6 +20,9 @@ function (VideoPlayer) {
|
||||
if (state.storage) {
|
||||
state.storage.clear();
|
||||
}
|
||||
if (state.videoPlayer) {
|
||||
_.result(state.videoPlayer, 'destroy');
|
||||
}
|
||||
});
|
||||
|
||||
describe('constructor', function () {
|
||||
@@ -47,7 +49,7 @@ function (VideoPlayer) {
|
||||
expect(state.videoCaption).toBeDefined();
|
||||
expect(state.speed).toEqual('1.50');
|
||||
expect(state.config.transcriptTranslationUrl)
|
||||
.toEqual('/transcript/translation');
|
||||
.toEqual('/transcript/translation/__lang__');
|
||||
});
|
||||
|
||||
it('create video speed control', function () {
|
||||
@@ -71,18 +73,15 @@ function (VideoPlayer) {
|
||||
var events;
|
||||
|
||||
jasmine.stubRequests();
|
||||
|
||||
spyOn(window.YT, 'Player').andCallThrough();
|
||||
|
||||
state = jasmine.initializePlayerYouTube();
|
||||
|
||||
state.videoEl = $('video, iframe');
|
||||
|
||||
events = {
|
||||
onReady: state.videoPlayer.onReady,
|
||||
onStateChange: state.videoPlayer.onStateChange,
|
||||
onPlaybackQualityChange: state.videoPlayer
|
||||
.onPlaybackQualityChange
|
||||
onPlaybackQualityChange: state.videoPlayer.onPlaybackQualityChange,
|
||||
onError: state.videoPlayer.onError
|
||||
};
|
||||
|
||||
expect(YT.Player).toHaveBeenCalledWith('id', {
|
||||
@@ -156,7 +155,7 @@ function (VideoPlayer) {
|
||||
});
|
||||
|
||||
it('controls are in paused state', function () {
|
||||
expect(state.videoControl.isPlaying).toBe(false);
|
||||
expect(state.videoPlayer.isPlaying()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -166,16 +165,10 @@ function (VideoPlayer) {
|
||||
state = jasmine.initializePlayer();
|
||||
|
||||
state.videoEl = $('video, iframe');
|
||||
|
||||
spyOn(state.videoPlayer, 'log').andCallThrough();
|
||||
spyOn(state.videoPlayer, 'play').andCallThrough();
|
||||
state.videoPlayer.onReady();
|
||||
});
|
||||
|
||||
it('log the load_video event', function () {
|
||||
expect(state.videoPlayer.log).toHaveBeenCalledWith('load_video');
|
||||
});
|
||||
|
||||
it('autoplay the first video', function () {
|
||||
expect(state.videoPlayer.play).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -197,9 +190,7 @@ function (VideoPlayer) {
|
||||
var playbackRates = state.videoPlayer.player.getAvailablePlaybackRates();
|
||||
|
||||
state.currentPlayerMode = 'flash';
|
||||
|
||||
state.videoPlayer.onReady();
|
||||
|
||||
expect(playbackRates.length).toBe(4);
|
||||
expect(state.currentPlayerMode).toBe('html5');
|
||||
});
|
||||
@@ -209,10 +200,7 @@ function (VideoPlayer) {
|
||||
describe('when the video is unstarted', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
|
||||
state.videoEl = $('video, iframe');
|
||||
|
||||
spyOn(state.videoControl, 'pause').andCallThrough();
|
||||
spyOn($.fn, 'trigger').andCallThrough();
|
||||
|
||||
state.videoPlayer.onStateChange({
|
||||
@@ -221,7 +209,7 @@ function (VideoPlayer) {
|
||||
});
|
||||
|
||||
it('pause the video control', function () {
|
||||
expect(state.videoControl.pause).toHaveBeenCalled();
|
||||
expect($('.video_control')).toHaveClass('play');
|
||||
});
|
||||
|
||||
it('pause the video caption', function () {
|
||||
@@ -244,9 +232,7 @@ function (VideoPlayer) {
|
||||
|
||||
state.videoEl = $('video, iframe');
|
||||
|
||||
spyOn(state.videoPlayer, 'log').andCallThrough();
|
||||
spyOn(window, 'setInterval').andReturn(100);
|
||||
spyOn(state.videoControl, 'play');
|
||||
spyOn($.fn, 'trigger').andCallThrough();
|
||||
|
||||
state.videoPlayer.onStateChange({
|
||||
@@ -254,23 +240,6 @@ function (VideoPlayer) {
|
||||
});
|
||||
});
|
||||
|
||||
it('speed_change_video event is not logged when speed not change', function () {
|
||||
expect(state.videoPlayer.log).not.toHaveBeenCalledWith(
|
||||
'speed_change_video',
|
||||
{
|
||||
current_time: state.videoPlayer.currentTime,
|
||||
old_speed: state.speed,
|
||||
new_speed: state.speed
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('log the play_video event', function () {
|
||||
expect(state.videoPlayer.log).toHaveBeenCalledWith(
|
||||
'play_video', { currentTime: 0 }
|
||||
);
|
||||
});
|
||||
|
||||
it('set update interval', function () {
|
||||
expect(window.setInterval).toHaveBeenCalledWith(
|
||||
state.videoPlayer.update, 200
|
||||
@@ -279,7 +248,7 @@ function (VideoPlayer) {
|
||||
});
|
||||
|
||||
it('play the video control', function () {
|
||||
expect(state.videoControl.play).toHaveBeenCalled();
|
||||
expect($('.video_control')).toHaveClass('pause');
|
||||
});
|
||||
|
||||
it('play the video caption', function () {
|
||||
@@ -295,10 +264,7 @@ function (VideoPlayer) {
|
||||
|
||||
state.videoEl = $('video, iframe');
|
||||
|
||||
spyOn(state.videoPlayer, 'log').andCallThrough();
|
||||
spyOn(state.videoControl, 'pause').andCallThrough();
|
||||
spyOn($.fn, 'trigger').andCallThrough();
|
||||
|
||||
state.videoPlayer.onStateChange({
|
||||
data: YT.PlayerState.PLAYING
|
||||
});
|
||||
@@ -310,18 +276,12 @@ function (VideoPlayer) {
|
||||
});
|
||||
});
|
||||
|
||||
it('log the pause_video event', function () {
|
||||
expect(state.videoPlayer.log).toHaveBeenCalledWith(
|
||||
'pause_video', { currentTime: 0 }
|
||||
);
|
||||
});
|
||||
|
||||
it('clear update interval', function () {
|
||||
expect(state.videoPlayer.updateInterval).toBeUndefined();
|
||||
});
|
||||
|
||||
it('pause the video control', function () {
|
||||
expect(state.videoControl.pause).toHaveBeenCalled();
|
||||
expect($('.video_control')).toHaveClass('play');
|
||||
});
|
||||
|
||||
it('pause the video caption', function () {
|
||||
@@ -334,32 +294,19 @@ function (VideoPlayer) {
|
||||
state = jasmine.initializePlayer();
|
||||
|
||||
state.videoEl = $('video, iframe');
|
||||
|
||||
spyOn(state.videoPlayer, 'log').andCallThrough();
|
||||
spyOn(state.videoControl, 'pause').andCallThrough();
|
||||
spyOn($.fn, 'trigger').andCallThrough();
|
||||
|
||||
state.videoPlayer.onStateChange({
|
||||
data: YT.PlayerState.ENDED
|
||||
});
|
||||
});
|
||||
|
||||
it('pause the video control', function () {
|
||||
expect(state.videoControl.pause).toHaveBeenCalled();
|
||||
expect($('.video_control')).toHaveClass('play');
|
||||
});
|
||||
|
||||
it('pause the video caption', function () {
|
||||
expect($.fn.trigger).toHaveBeenCalledWith('ended', {});
|
||||
});
|
||||
|
||||
it('log stop_video event', function () {
|
||||
expect(state.videoPlayer.log).toHaveBeenCalledWith(
|
||||
'stop_video',
|
||||
{
|
||||
currentTime: state.videoPlayer.currentTime
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -397,25 +344,6 @@ function (VideoPlayer) {
|
||||
});
|
||||
});
|
||||
|
||||
it('slider event causes log update', function () {
|
||||
runs(function () {
|
||||
spyOn(state.videoPlayer, 'log');
|
||||
state.videoProgressSlider.onSlide(
|
||||
jQuery.Event('slide'), { value: 2 }
|
||||
);
|
||||
// Video player uses _.debounce (with a wait time in 300 ms) for seeking.
|
||||
// That's why we have to do this tick(300).
|
||||
jasmine.Clock.tick(300);
|
||||
expect(state.videoPlayer.currentTime).toBe(2);
|
||||
|
||||
expect(state.videoPlayer.log).toHaveBeenCalledWith('seek_video', {
|
||||
old_time: jasmine.any(Number),
|
||||
new_time: 2,
|
||||
type: 'onSlideSeek'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('seek the player', function () {
|
||||
runs(function () {
|
||||
spyOn(state.videoPlayer.player, 'seekTo').andCallThrough();
|
||||
@@ -469,24 +397,6 @@ function (VideoPlayer) {
|
||||
.andCallThrough();
|
||||
});
|
||||
|
||||
it('slider event causes log update', function () {
|
||||
spyOn(state.videoPlayer, 'log');
|
||||
state.videoProgressSlider.onSlide(
|
||||
jQuery.Event('slide'), { value: 2 }
|
||||
);
|
||||
// Video player uses _.debounce (with a wait time in 300 ms) for seeking.
|
||||
// That's why we have to do this tick(300).
|
||||
jasmine.Clock.tick(300);
|
||||
expect(state.videoPlayer.currentTime).toBe(2);
|
||||
expect(state.videoPlayer.log).toHaveBeenCalledWith(
|
||||
'seek_video', {
|
||||
old_time: 0,
|
||||
new_time: 2,
|
||||
type: 'onSlideSeek'
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('video has a correct speed', function () {
|
||||
state.speed = '2.0';
|
||||
state.videoPlayer.onPlay();
|
||||
@@ -785,7 +695,7 @@ function (VideoPlayer) {
|
||||
state = jasmine.initializePlayer();
|
||||
state.videoEl = $('video, iframe');
|
||||
spyOn($.fn, 'trigger').andCallThrough();
|
||||
state.videoControl.toggleFullScreen(jQuery.Event('click'));
|
||||
$('.add-fullscreen').click();
|
||||
});
|
||||
|
||||
it('replace the full screen button tooltip', function () {
|
||||
@@ -810,11 +720,10 @@ function (VideoPlayer) {
|
||||
state.videoEl = $('video, iframe');
|
||||
spyOn($.fn, 'trigger').andCallThrough();
|
||||
state.el.addClass('video-fullscreen');
|
||||
state.videoControl.fullScreenState = true;
|
||||
state.videoControl.isFullScreen = true;
|
||||
state.videoControl.fullScreenEl.attr('title', 'Exit-fullscreen');
|
||||
|
||||
state.videoControl.toggleFullScreen(jQuery.Event('click'));
|
||||
state.videoFullScreen.fullScreenState = true;
|
||||
state.videoFullScreen.isFullScreen = true;
|
||||
state.videoFullScreen.fullScreenEl.attr('title', 'Exit-fullscreen');
|
||||
$('.add-fullscreen').click();
|
||||
});
|
||||
|
||||
it('replace the full screen button tooltip', function () {
|
||||
@@ -835,83 +744,6 @@ function (VideoPlayer) {
|
||||
});
|
||||
});
|
||||
|
||||
describe('play', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
|
||||
state.videoEl = $('video, iframe');
|
||||
|
||||
spyOn(state.videoPlayer.player, 'playVideo').andCallThrough();
|
||||
});
|
||||
|
||||
describe('when the player is not ready', function () {
|
||||
beforeEach(function () {
|
||||
state.videoPlayer.player.playVideo = void 0;
|
||||
state.videoPlayer.play();
|
||||
});
|
||||
|
||||
it('does nothing', function () {
|
||||
expect(state.videoPlayer.player.playVideo).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the player is ready', function () {
|
||||
beforeEach(function () {
|
||||
state.videoPlayer.player.playVideo.andReturn(true);
|
||||
state.videoPlayer.play();
|
||||
});
|
||||
|
||||
it('delegate to the player', function () {
|
||||
expect(state.videoPlayer.player.playVideo).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPlaying', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
|
||||
state.videoEl = $('video, iframe');
|
||||
|
||||
spyOn(state.videoPlayer.player, 'getPlayerState').andCallThrough();
|
||||
});
|
||||
|
||||
describe('when the video is playing', function () {
|
||||
beforeEach(function () {
|
||||
state.videoPlayer.player.getPlayerState.andReturn(YT.PlayerState.PLAYING);
|
||||
});
|
||||
|
||||
it('return true', function () {
|
||||
expect(state.videoPlayer.isPlaying()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the video is not playing', function () {
|
||||
beforeEach(function () {
|
||||
state.videoPlayer.player.getPlayerState.andReturn(YT.PlayerState.PAUSED);
|
||||
});
|
||||
|
||||
it('return false', function () {
|
||||
expect(state.videoPlayer.isPlaying()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pause', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
|
||||
state.videoEl = $('video, iframe');
|
||||
|
||||
spyOn(state.videoPlayer.player, 'pauseVideo').andCallThrough();
|
||||
state.videoPlayer.pause();
|
||||
});
|
||||
|
||||
it('delegate to the player', function () {
|
||||
expect(state.videoPlayer.player.pauseVideo).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('duration', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
@@ -1016,9 +848,7 @@ function (VideoPlayer) {
|
||||
|
||||
runs(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
|
||||
state.videoEl = $('video, iframe');
|
||||
|
||||
controls = state.el.find('.video-controls');
|
||||
});
|
||||
|
||||
@@ -1053,7 +883,6 @@ function (VideoPlayer) {
|
||||
saveState: jasmine.createSpy(),
|
||||
videoPlayer: {
|
||||
currentTime: 60,
|
||||
log: jasmine.createSpy(),
|
||||
updatePlayTime: jasmine.createSpy(),
|
||||
setPlaybackRate: jasmine.createSpy(),
|
||||
player: jasmine.createSpyObj('player', ['setPlaybackRate'])
|
||||
@@ -1063,18 +892,6 @@ function (VideoPlayer) {
|
||||
});
|
||||
|
||||
describe('always', function () {
|
||||
it('check if speed_change_video is logged', function () {
|
||||
VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false);
|
||||
expect(state.videoPlayer.log).toHaveBeenCalledWith(
|
||||
'speed_change_video',
|
||||
{
|
||||
current_time: state.videoPlayer.currentTime,
|
||||
old_speed: '1.50',
|
||||
new_speed: '0.75'
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('convert the current time to the new speed', function () {
|
||||
state.isFlashMode.andReturn(true);
|
||||
VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false);
|
||||
@@ -1083,10 +900,7 @@ function (VideoPlayer) {
|
||||
|
||||
it('set video speed to the new speed', function () {
|
||||
VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false);
|
||||
expect(state.setSpeed).toHaveBeenCalledWith('0.75', true);
|
||||
expect(state.saveState).toHaveBeenCalledWith(true, {
|
||||
speed: '0.75'
|
||||
});
|
||||
expect(state.setSpeed).toHaveBeenCalledWith('0.75');
|
||||
expect(state.videoPlayer.setPlaybackRate)
|
||||
.toHaveBeenCalledWith('0.75');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
(function (WAIT_TIMEOUT) {
|
||||
'use strict';
|
||||
describe('VideoPoster', function () {
|
||||
var state, oldOTBD;
|
||||
|
||||
beforeEach(function () {
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine
|
||||
.createSpy('onTouchBasedDevice').andReturn(null);
|
||||
state = jasmine.initializePlayer('video_with_bumper.html');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
$('source').remove();
|
||||
state.storage.clear();
|
||||
if (state.bumperState && state.bumperState.videoPlayer) {
|
||||
state.bumperState.videoPlayer.destroy();
|
||||
}
|
||||
if (state.videoPlayer) {
|
||||
state.videoPlayer.destroy();
|
||||
}
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
});
|
||||
|
||||
it('can render the poster', function () {
|
||||
expect($('.poster')).toExist();
|
||||
expect($('.btn-play')).toExist();
|
||||
});
|
||||
|
||||
it('can start playing the video on click', function () {
|
||||
$('.btn-play').click();
|
||||
waitsFor(function () {
|
||||
return state.el.hasClass('is-playing');
|
||||
}, 'Player is not playing.', WAIT_TIMEOUT);
|
||||
});
|
||||
|
||||
it('destroy itself on "play" event', function () {
|
||||
$('.btn-play').click();
|
||||
expect($('.poster')).not.toExist();
|
||||
});
|
||||
});
|
||||
}).call(this, window.WAIT_TIMEOUT);
|
||||
@@ -12,6 +12,7 @@
|
||||
$('source').remove();
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
state.storage.clear();
|
||||
state.videoPlayer.destroy();
|
||||
});
|
||||
|
||||
describe('constructor', function () {
|
||||
@@ -38,6 +39,18 @@
|
||||
expect(state.videoProgressSlider.handle)
|
||||
.toBe('.slider .ui-slider-handle');
|
||||
});
|
||||
|
||||
it('add ARIA attributes to time control', function () {
|
||||
var timeControl = $('div.slider > a');
|
||||
|
||||
expect(timeControl).toHaveAttrs({
|
||||
'role': 'slider',
|
||||
'title': 'Video position',
|
||||
'aria-disabled': 'false'
|
||||
});
|
||||
|
||||
expect(timeControl).toHaveAttr('aria-valuetext');
|
||||
});
|
||||
});
|
||||
|
||||
describe('on a touch-based device', function () {
|
||||
@@ -304,6 +317,13 @@
|
||||
});
|
||||
});
|
||||
|
||||
it('can destroy itself', function () {
|
||||
state = jasmine.initializePlayer();
|
||||
state.videoProgressSlider.destroy();
|
||||
expect(state.videoProgressSlider).toBeUndefined();
|
||||
expect($('.slider')).toBeEmpty();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
(function (undefined) {
|
||||
describe('VideoQualityControl', function () {
|
||||
var state, qualityControl, qualityControlEl, videoPlayer, player;
|
||||
var state, qualityControl, videoPlayer, player;
|
||||
|
||||
afterEach(function () {
|
||||
$('source').remove();
|
||||
if (state.storage) {
|
||||
state.storage.clear();
|
||||
}
|
||||
state.videoPlayer.destroy();
|
||||
});
|
||||
|
||||
describe('constructor, YouTube mode', function () {
|
||||
@@ -105,6 +106,11 @@
|
||||
expect(qualityControl.el).toHaveClass('active');
|
||||
});
|
||||
|
||||
it('can destroy itself', function () {
|
||||
state.videoQualityControl.destroy();
|
||||
expect(state.videoQualityControl).toBeUndefined();
|
||||
expect($('.quality-control')).not.toExist();
|
||||
});
|
||||
});
|
||||
|
||||
describe('constructor, HTML5 mode', function () {
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
(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);
|
||||
@@ -0,0 +1,55 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
describe('VideoSkipControl', function () {
|
||||
var state, oldOTBD;
|
||||
|
||||
beforeEach(function () {
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine
|
||||
.createSpy('onTouchBasedDevice').andReturn(null);
|
||||
state = jasmine.initializePlayer('video_with_bumper.html');
|
||||
$('.poster .btn-play').click();
|
||||
spyOn(state.bumperState.videoCommands, 'execute').andCallThrough();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
$('source').remove();
|
||||
state.storage.clear();
|
||||
if (state.bumperState && state.bumperState.videoPlayer) {
|
||||
state.bumperState.videoPlayer.destroy();
|
||||
}
|
||||
if (state.videoPlayer) {
|
||||
state.videoPlayer.destroy();
|
||||
}
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
});
|
||||
|
||||
it('can render the control when video starts playing', function () {
|
||||
expect($('.skip-control')).not.toExist();
|
||||
state.el.trigger('play');
|
||||
expect($('.skip-control')).toExist();
|
||||
});
|
||||
|
||||
it('add ARIA attributes to play control', function () {
|
||||
state.el.trigger('play');
|
||||
expect($('.skip-control')).toHaveAttrs({
|
||||
'role': 'button',
|
||||
'title': 'Do not show again',
|
||||
'aria-disabled': 'false'
|
||||
});
|
||||
});
|
||||
|
||||
it('can skip the video on click', function () {
|
||||
spyOn(state.bumperState.videoBumper, 'skipAndDoNotShowAgain');
|
||||
state.el.trigger('play');
|
||||
$('.skip-control').click();
|
||||
expect(state.bumperState.videoCommands.execute).toHaveBeenCalledWith('skip', true);
|
||||
expect(state.bumperState.videoBumper.skipAndDoNotShowAgain).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can destroy itself', function () {
|
||||
state.bumperState.videoPlaySkipControl.destroy();
|
||||
expect(state.bumperState.videoPlaySkipControl).toBeUndefined();
|
||||
});
|
||||
});
|
||||
}).call(this);
|
||||
@@ -12,6 +12,7 @@
|
||||
$('source').remove();
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
state.storage.clear();
|
||||
state.videoPlayer.destroy();
|
||||
});
|
||||
|
||||
describe('constructor', function () {
|
||||
@@ -247,5 +248,13 @@
|
||||
expect($('.speeds .value')).toHaveHtml('0.75x');
|
||||
});
|
||||
});
|
||||
|
||||
it('can destroy itself', function () {
|
||||
state = jasmine.initializePlayer();
|
||||
state.videoSpeedControl.destroy();
|
||||
expect(state.videoSpeedControl).toBeUndefined();
|
||||
expect($('.video-speeds')).not.toExist();
|
||||
expect($('.speed-button')).not.toExist();
|
||||
});
|
||||
});
|
||||
}).call(this);
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('VideoVolumeControl', function () {
|
||||
$('source').remove();
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
state.storage.clear();
|
||||
state.videoPlayer.destroy();
|
||||
});
|
||||
|
||||
it('Volume level has correct value even if cookie is broken', function () {
|
||||
@@ -35,8 +36,7 @@ describe('VideoVolumeControl', function () {
|
||||
});
|
||||
|
||||
it('render the volume control', function () {
|
||||
expect(state.videoControl.secondaryControlsEl.html())
|
||||
.toContain('<div class="volume">\n');
|
||||
expect($('.volume')).toExist();
|
||||
});
|
||||
|
||||
it('create the slider', function () {
|
||||
@@ -292,7 +292,7 @@ describe('VideoVolumeControl', function () {
|
||||
shiftKey: true
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('keyDownButtonHandler', function () {
|
||||
beforeEach(function () {
|
||||
@@ -308,6 +308,6 @@ describe('VideoVolumeControl', function () {
|
||||
}));
|
||||
expect(volumeControl.getMuteStatus()).toEqual(isMuted);
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
}).call(this);
|
||||
|
||||
@@ -177,9 +177,8 @@ function () {
|
||||
}
|
||||
};
|
||||
|
||||
var cleanDelta = function () {
|
||||
delta['height'] = 0;
|
||||
delta['width'] = 0;
|
||||
var resetDelta = function () {
|
||||
delta['height'] = delta['width'] = 0;
|
||||
|
||||
return module;
|
||||
};
|
||||
@@ -200,12 +199,23 @@ function () {
|
||||
return module;
|
||||
};
|
||||
|
||||
var destroy = function () {
|
||||
var data = getData();
|
||||
data.element.css({
|
||||
'height': '', 'width': '', 'top': '', 'left': ''
|
||||
});
|
||||
removeCallbacks();
|
||||
resetDelta();
|
||||
mode = null;
|
||||
};
|
||||
|
||||
initialize.apply(module, arguments);
|
||||
|
||||
return $.extend(true, module, {
|
||||
align: align,
|
||||
alignByWidthOnly: alignByWidthOnly,
|
||||
alignByHeightOnly: alignByHeightOnly,
|
||||
destroy: destroy,
|
||||
setParams: initialize,
|
||||
setMode: setMode,
|
||||
setElement: setElement,
|
||||
@@ -218,7 +228,7 @@ function () {
|
||||
delta: {
|
||||
add: addDelta,
|
||||
substract: substractDelta,
|
||||
reset: cleanDelta
|
||||
reset: resetDelta
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
define(
|
||||
'video/01_initialize.js',
|
||||
['video/03_video_player.js', 'video/00_video_storage.js', 'video/00_i18n.js'],
|
||||
function (VideoPlayer, VideoStorage, i18n) {
|
||||
['video/03_video_player.js', 'video/00_i18n.js'],
|
||||
function (VideoPlayer, i18n) {
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
@@ -71,7 +71,6 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
isYoutubeType: isYoutubeType,
|
||||
parseSpeed: parseSpeed,
|
||||
parseYoutubeStreams: parseYoutubeStreams,
|
||||
saveState: saveState,
|
||||
setPlayerMode: setPlayerMode,
|
||||
setSpeed: setSpeed,
|
||||
speedToString: speedToString,
|
||||
@@ -145,9 +144,7 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
_youtubeApiDeferred.resolve();
|
||||
}
|
||||
|
||||
window.YT.ready(function () {
|
||||
onYTApiReady();
|
||||
});
|
||||
window.YT.ready(onYTApiReady);
|
||||
} else {
|
||||
// There is only one global variable window.onYouTubeIframeAPIReady which
|
||||
// is supposed to be a function that will be called by the YouTube API
|
||||
@@ -191,9 +188,7 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
// Attach a callback to our Deferred object to be called once the
|
||||
// YouTube API loads.
|
||||
window.onYouTubeIframeAPIReady.done(function () {
|
||||
window.YT.ready(function () {
|
||||
onYTApiReady();
|
||||
});
|
||||
window.YT.ready(onYTApiReady);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -212,20 +207,15 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
// callback, which will set `state.youtubeApiAvailable` to `true`.
|
||||
// If something goes wrong at this stage, `state.youtubeApiAvailable` is
|
||||
// `false`.
|
||||
_reportToServer(state, state.youtubeApiAvailable);
|
||||
if (!state.youtubeIsAvailable) {
|
||||
console.log('[Video info]: YouTube API is not available.');
|
||||
}
|
||||
state.el.trigger('youtube_availability', [state.youtubeIsAvailable]);
|
||||
}, state.config.ytTestTimeout);
|
||||
|
||||
$.getScript(document.location.protocol + '//' + state.config.ytApiUrl);
|
||||
}
|
||||
|
||||
function _reportToServer(state, youtubeIsAvailable) {
|
||||
if (!youtubeIsAvailable) {
|
||||
console.log('[Video info]: YouTube API is not available.');
|
||||
}
|
||||
|
||||
state.saveState(true, { youtube_is_available: youtubeIsAvailable });
|
||||
}
|
||||
|
||||
// function _configureCaptions(state)
|
||||
// Configure displaying of captions.
|
||||
//
|
||||
@@ -296,8 +286,7 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
|
||||
state.videoType = 'html5';
|
||||
|
||||
if (!state.config.sub || !state.config.sub.length) {
|
||||
state.config.sub = '';
|
||||
if (!_.keys(state.config.transcriptLanguages).length) {
|
||||
state.config.showCaptions = false;
|
||||
}
|
||||
state.setSpeed(state.speed);
|
||||
@@ -328,8 +317,9 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
function _initializeModules(state, i18n) {
|
||||
var dfd = $.Deferred(),
|
||||
modulesList = $.map(state.modules, function(module) {
|
||||
if ($.isFunction(module)) {
|
||||
return module(state, i18n);
|
||||
var options = state.options[module.moduleName] || {};
|
||||
if (_.isFunction(module)) {
|
||||
return module(state, i18n, options);
|
||||
} else if ($.isPlainObject(module)) {
|
||||
return module;
|
||||
}
|
||||
@@ -388,7 +378,6 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
},
|
||||
'startTime': function (value) {
|
||||
value = parseInt(value, 10);
|
||||
|
||||
if (!isFinite(value) || value < 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -407,6 +396,13 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
},
|
||||
config = {};
|
||||
|
||||
data = _.extend({
|
||||
startTime: 0,
|
||||
endTime: null,
|
||||
sub: '',
|
||||
streams: ''
|
||||
}, data);
|
||||
|
||||
$.each(data, function(option, value) {
|
||||
// Extract option that is in `extractKeys`.
|
||||
if ($.inArray(option, extractKeys) !== -1) {
|
||||
@@ -420,7 +416,7 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
|
||||
// Pre-process data.
|
||||
if (conversions[option]) {
|
||||
if ($.isFunction(conversions[option])) {
|
||||
if (_.isFunction(conversions[option])) {
|
||||
value = conversions[option].call(this, value);
|
||||
} else {
|
||||
throw new TypeError(option + ' is not a function.');
|
||||
@@ -463,12 +459,11 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
|
||||
function initialize(element) {
|
||||
var self = this,
|
||||
el = $(element).find('.video'),
|
||||
el = this.el,
|
||||
id = this.id,
|
||||
container = el.find('.video-wrapper'),
|
||||
id = el.attr('id').replace(/video_/, ''),
|
||||
__dfd__ = $.Deferred(),
|
||||
isTouch = onTouchBasedDevice() || '',
|
||||
storage = VideoStorage('VideoState', id);
|
||||
isTouch = onTouchBasedDevice() || '';
|
||||
|
||||
if (isTouch) {
|
||||
el.addClass('is-touch');
|
||||
@@ -476,23 +471,18 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
|
||||
$.extend(this, {
|
||||
__dfd__: __dfd__,
|
||||
el: el,
|
||||
container: container,
|
||||
id: id,
|
||||
isFullScreen: false,
|
||||
isTouch: isTouch,
|
||||
storage: storage
|
||||
isTouch: isTouch
|
||||
});
|
||||
|
||||
console.log(
|
||||
'[Video info]: Initializing video with id "' + id + '".'
|
||||
);
|
||||
console.log('[Video info]: Initializing video with id "%s".', id);
|
||||
|
||||
// We store all settings passed to us by the server in one place. These
|
||||
// are "read only", so don't modify them. All variable content lives in
|
||||
// 'state' object.
|
||||
// jQuery .data() return object with keys in lower camelCase format.
|
||||
this.config = $.extend({}, _getConfiguration(el.data(), storage), {
|
||||
this.config = $.extend({}, _getConfiguration(this.metadata, this.storage), {
|
||||
element: element,
|
||||
fadeOutTimeout: 1400,
|
||||
captionsFreezeTime: 10000,
|
||||
@@ -602,26 +592,18 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
// true: Parsing of YouTube video IDs went OK, and we can proceed
|
||||
// onwards to play YouTube videos.
|
||||
function parseYoutubeStreams(youtubeStreams) {
|
||||
var _this;
|
||||
|
||||
if (
|
||||
typeof youtubeStreams === 'undefined' ||
|
||||
youtubeStreams.length === 0
|
||||
) {
|
||||
if (_.isUndefined(youtubeStreams) || !youtubeStreams.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_this = this;
|
||||
this.videos = {};
|
||||
|
||||
$.each(youtubeStreams.split(/,/), function (index, video) {
|
||||
_.each(youtubeStreams.split(/,/), function (video) {
|
||||
var speed;
|
||||
|
||||
video = video.split(/:/);
|
||||
speed = _this.speedToString(video[0]);
|
||||
|
||||
_this.videos[speed] = video[1];
|
||||
});
|
||||
speed = this.speedToString(video[0]);
|
||||
this.videos[speed] = video[1];
|
||||
}, this);
|
||||
|
||||
return _.isString(this.videos['1.0']);
|
||||
}
|
||||
@@ -633,23 +615,21 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
// example the length of the video can be determined from the meta
|
||||
// data.
|
||||
function fetchMetadata() {
|
||||
var _this = this,
|
||||
var self = this,
|
||||
metadataXHRs = [];
|
||||
|
||||
this.metadata = {};
|
||||
|
||||
$.each(this.videos, function (speed, url) {
|
||||
var xhr = _this.getVideoMetadata(url, function (data) {
|
||||
metadataXHRs = _.map(this.videos, function (url, speed) {
|
||||
return self.getVideoMetadata(url, function (data) {
|
||||
if (data.data) {
|
||||
_this.metadata[data.data.id] = data.data;
|
||||
self.metadata[data.data.id] = data.data;
|
||||
}
|
||||
});
|
||||
|
||||
metadataXHRs.push(xhr);
|
||||
});
|
||||
|
||||
$.when.apply(this, metadataXHRs).done(function () {
|
||||
_this.el.trigger('metadata_received');
|
||||
self.el.trigger('metadata_received');
|
||||
|
||||
// Not only do we trigger the "metadata_received" event, we also
|
||||
// set a flag to notify that metadata has been received. This
|
||||
@@ -657,7 +637,7 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
// to know that metadata has been received. This is important in
|
||||
// cases when some code will subscribe to the "metadata_received"
|
||||
// event after it has been triggered.
|
||||
_this.youtubeMetadataReceived = true;
|
||||
self.youtubeMetadataReceived = true;
|
||||
|
||||
});
|
||||
}
|
||||
@@ -666,23 +646,21 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
//
|
||||
// Create a separate array of available speeds.
|
||||
function parseSpeed() {
|
||||
this.speeds = ($.map(this.videos, function (url, speed) {
|
||||
return speed;
|
||||
})).sort();
|
||||
this.speeds = _.keys(this.videos).sort();
|
||||
}
|
||||
|
||||
function setSpeed(newSpeed, updateStorage) {
|
||||
function setSpeed(newSpeed) {
|
||||
// Possible speeds for each player type.
|
||||
// HTML5 = [0.75, 1, 1.25, 1.5]
|
||||
// Youtube Flash = [0.75, 1, 1.25, 1.5]
|
||||
// Youtube HTML5 = [0.25, 0.5, 1, 1.5, 2]
|
||||
var map = {
|
||||
'0.25': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
|
||||
'0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
|
||||
'0.75': '0.50', // HTML5 or Youtube Flash -> Youtube HTML5
|
||||
'1.25': '1.50', // HTML5 or Youtube Flash -> Youtube HTML5
|
||||
'2.0': '1.50' // Youtube HTML5 -> HTML5 or Youtube Flash
|
||||
};
|
||||
'0.25': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
|
||||
'0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
|
||||
'0.75': '0.50', // HTML5 or Youtube Flash -> Youtube HTML5
|
||||
'1.25': '1.50', // HTML5 or Youtube Flash -> Youtube HTML5
|
||||
'2.0': '1.50' // Youtube HTML5 -> HTML5 or Youtube Flash
|
||||
};
|
||||
|
||||
if (_.contains(this.speeds, newSpeed)) {
|
||||
this.speed = newSpeed;
|
||||
@@ -690,57 +668,21 @@ function (VideoPlayer, VideoStorage, i18n) {
|
||||
newSpeed = map[newSpeed];
|
||||
this.speed = _.contains(this.speeds, newSpeed) ? newSpeed : '1.0';
|
||||
}
|
||||
|
||||
if (updateStorage) {
|
||||
this.storage.setItem('speed', this.speed, true);
|
||||
this.storage.setItem('general_speed', this.speed);
|
||||
}
|
||||
}
|
||||
|
||||
function getVideoMetadata(url, callback) {
|
||||
var successHandler, xhr;
|
||||
|
||||
if (typeof url !== 'string') {
|
||||
if (!(_.isString(url))) {
|
||||
url = this.videos['1.0'] || '';
|
||||
}
|
||||
successHandler = ($.isFunction(callback)) ? callback : null;
|
||||
xhr = $.ajax({
|
||||
|
||||
return $.ajax({
|
||||
url: [
|
||||
document.location.protocol, '//', this.config.ytTestUrl, url,
|
||||
'?v=2&alt=jsonc'
|
||||
].join(''),
|
||||
dataType: 'jsonp',
|
||||
timeout: this.config.ytTestTimeout,
|
||||
success: successHandler
|
||||
});
|
||||
|
||||
return xhr;
|
||||
}
|
||||
|
||||
function saveState(async, data) {
|
||||
|
||||
if (!($.isPlainObject(data))) {
|
||||
data = {
|
||||
saved_video_position: this.videoPlayer.currentTime
|
||||
};
|
||||
}
|
||||
|
||||
if (data.speed) {
|
||||
this.storage.setItem('speed', data.speed, true);
|
||||
}
|
||||
|
||||
if (data.hasOwnProperty('saved_video_position')) {
|
||||
this.storage.setItem('savedVideoPosition', data.saved_video_position, true);
|
||||
|
||||
data.saved_video_position = Time.formatFull(data.saved_video_position);
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.saveStateUrl,
|
||||
type: 'POST',
|
||||
async: async ? true : false,
|
||||
dataType: 'json',
|
||||
data: data,
|
||||
success: _.isFunction(callback) ? callback : null
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,54 @@ function () {
|
||||
});
|
||||
};
|
||||
|
||||
Player.prototype.onError = function (event) {
|
||||
if ($.isFunction(this.config.events.onError)) {
|
||||
this.config.events.onError();
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.destroy = function () {
|
||||
this.video.removeEventListener('loadedmetadata', this.onLoadedMetadata, false);
|
||||
this.video.removeEventListener('play', this.onPlay, false);
|
||||
this.video.removeEventListener('playing', this.onPlaying, false);
|
||||
this.video.removeEventListener('pause', this.onPause, false);
|
||||
this.video.removeEventListener('ended', this.onEnded, false);
|
||||
this.el
|
||||
.find('.video-player div').removeClass('hidden')
|
||||
.end()
|
||||
.find('.video-player h3').addClass('hidden')
|
||||
.end().removeClass('is-initialized')
|
||||
.find('.spinner').attr({'aria-hidden': 'false'});
|
||||
this.videoEl.remove();
|
||||
};
|
||||
|
||||
Player.prototype.onLoadedMetadata = function () {
|
||||
this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
if ($.isFunction(this.config.events.onReady)) {
|
||||
this.config.events.onReady(null);
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.onPlay = function () {
|
||||
this.playerState = HTML5Video.PlayerState.BUFFERING;
|
||||
this.callStateChangeCallback();
|
||||
};
|
||||
|
||||
Player.prototype.onPlaying = function () {
|
||||
this.playerState = HTML5Video.PlayerState.PLAYING;
|
||||
this.callStateChangeCallback();
|
||||
};
|
||||
|
||||
Player.prototype.onPause = function () {
|
||||
this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
this.callStateChangeCallback();
|
||||
};
|
||||
|
||||
Player.prototype.onEnded = function () {
|
||||
this.playerState = HTML5Video.PlayerState.ENDED;
|
||||
this.callStateChangeCallback();
|
||||
};
|
||||
|
||||
return Player;
|
||||
|
||||
/*
|
||||
@@ -152,6 +200,7 @@ function () {
|
||||
var isTouch = onTouchBasedDevice() || '',
|
||||
sourceList, _this, errorMessage, lastSource;
|
||||
|
||||
_.bindAll(this, 'onLoadedMetadata', 'onPlay', 'onPlaying', 'onPause', 'onEnded');
|
||||
this.logs = [];
|
||||
// Initially we assume that el is a DOM element. If jQuery selector
|
||||
// fails to select something, we assume that el is an ID of a DOM
|
||||
@@ -226,6 +275,8 @@ function () {
|
||||
|
||||
lastSource = this.videoEl.find('source').last();
|
||||
lastSource.on('error', this.showErrorMessage.bind(this));
|
||||
lastSource.on('error', this.onError.bind(this));
|
||||
this.videoEl.on('error', this.onError.bind(this));
|
||||
|
||||
if (/iP(hone|od)/i.test(isTouch[0])) {
|
||||
this.videoEl.prop('controls', true);
|
||||
@@ -280,35 +331,11 @@ function () {
|
||||
// When the <video> tag has been processed by the browser, and it
|
||||
// is ready for playback, notify other parts of the VideoPlayer,
|
||||
// and initially pause the video.
|
||||
this.video.addEventListener('loadedmetadata', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
if ($.isFunction(_this.config.events.onReady)) {
|
||||
_this.config.events.onReady(null);
|
||||
}
|
||||
}, false);
|
||||
|
||||
// Register the 'play' event.
|
||||
this.video.addEventListener('play', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.BUFFERING;
|
||||
_this.callStateChangeCallback();
|
||||
}, false);
|
||||
|
||||
this.video.addEventListener('playing', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.PLAYING;
|
||||
_this.callStateChangeCallback();
|
||||
}, false);
|
||||
|
||||
// Register the 'pause' event.
|
||||
this.video.addEventListener('pause', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
_this.callStateChangeCallback();
|
||||
}, false);
|
||||
|
||||
// Register the 'ended' event.
|
||||
this.video.addEventListener('ended', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.ENDED;
|
||||
_this.callStateChangeCallback();
|
||||
}, false);
|
||||
this.video.addEventListener('loadedmetadata', this.onLoadedMetadata, false);
|
||||
this.video.addEventListener('play', this.onPlay, false);
|
||||
this.video.addEventListener('playing', this.onPlaying, false);
|
||||
this.video.addEventListener('pause', this.onPause, false);
|
||||
this.video.addEventListener('ended', this.onEnded, false);
|
||||
|
||||
// Place the <video> element on the page.
|
||||
this.videoEl.appendTo(this.el.find('.video-player div'));
|
||||
|
||||
@@ -1,308 +1,241 @@
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
(function(define) {
|
||||
'use strict';
|
||||
// VideoAccessibleMenu module.
|
||||
define(
|
||||
'video/035_video_accessible_menu.js',
|
||||
[],
|
||||
function () {
|
||||
|
||||
// VideoAccessibleMenu() function - what this module "exports".
|
||||
return function (state) {
|
||||
var dfd = $.Deferred();
|
||||
|
||||
if (state.el.find('li.video-tracks') === 0) {
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
'video/035_video_accessible_menu.js', [],
|
||||
function() {
|
||||
/**
|
||||
* Video Download Transcript control module.
|
||||
* @exports video/035_video_accessible_menu.js
|
||||
* @constructor
|
||||
* @param {jquery Element} element
|
||||
* @param {Object} options
|
||||
*/
|
||||
var VideoAccessibleMenu = function(element, options) {
|
||||
if (!(this instanceof VideoAccessibleMenu)) {
|
||||
return new VideoAccessibleMenu(element, options);
|
||||
}
|
||||
|
||||
state.videoAccessibleMenu = {
|
||||
value: state.storage.getItem('transcript_download_format')
|
||||
};
|
||||
_.bindAll(this, 'openMenu', 'openMenuHandler', 'closeMenu', 'closeMenuHandler', 'toggleMenuHandler',
|
||||
'clickHandler', 'keyDownHandler', 'render', 'menuItemsLinksFocused', 'changeFileType', 'setValue'
|
||||
);
|
||||
|
||||
_initialize(state);
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
this.container = element;
|
||||
this.options = options || {};
|
||||
|
||||
if (this.container.find('.video-tracks')) {
|
||||
this.initialize();
|
||||
}
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
VideoAccessibleMenu.prototype = {
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
this.value = this.options.storage.getItem('transcript_download_format');
|
||||
this.el = this.container.find('.video-tracks .a11y-menu-container');
|
||||
this.render();
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
function _initialize(state) {
|
||||
_makeFunctionsPublic(state);
|
||||
_renderElements(state);
|
||||
_addAriaAttributes(state);
|
||||
_bindHandlers(state);
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
changeFileType: changeFileType,
|
||||
setValue: setValue
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.videoAccessibleMenu, state);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
// For the time being, we assume that the menu structure is present in
|
||||
// the template HTML. In the future accessible menu plugin, everything
|
||||
// inside <div class='menu-container'></div> will be generated in this
|
||||
// file.
|
||||
var container = state.el.find('li.video-tracks>div.a11y-menu-container'),
|
||||
button = container.children('a.a11y-menu-button'),
|
||||
menuList = container.children('ol.a11y-menu-list'),
|
||||
menuItems = menuList.children('li.a11y-menu-item'),
|
||||
menuItemsLinks = menuItems.children('a.a11y-menu-item-link'),
|
||||
/**
|
||||
* Creates any necessary DOM elements, attach them, and set their,
|
||||
* initial configuration.
|
||||
*/
|
||||
render: function() {
|
||||
var value, msg;
|
||||
// For the time being, we assume that the menu structure is present in
|
||||
// the template HTML. In the future accessible menu plugin, everything
|
||||
// inside <div class='menu-container'></div> will be generated in this
|
||||
// file.
|
||||
this.button = this.el.children('.a11y-menu-button');
|
||||
this.menuList = this.el.children('.a11y-menu-list');
|
||||
this.menuItems = this.menuList.children('.a11y-menu-item');
|
||||
this.menuItemsLinks = this.menuItems.children('.a11y-menu-item-link');
|
||||
value = (function (val, activeElement) {
|
||||
return val || activeElement.find('a').data('value') || 'srt';
|
||||
}(state.videoAccessibleMenu.value, menuItems.filter('.active'))),
|
||||
}(this.value, this.menuItems.filter('.active')));
|
||||
msg = '.' + value;
|
||||
|
||||
$.extend(state.videoAccessibleMenu, {
|
||||
container: container,
|
||||
button: button,
|
||||
menuList: menuList,
|
||||
menuItems: menuItems,
|
||||
menuItemsLinks: menuItemsLinks
|
||||
});
|
||||
if (value) {
|
||||
this.setValue(value);
|
||||
this.button.text(gettext(msg));
|
||||
}
|
||||
},
|
||||
|
||||
if (value) {
|
||||
state.videoAccessibleMenu.setValue(value);
|
||||
button.text(gettext(msg));
|
||||
}
|
||||
}
|
||||
|
||||
function _addAriaAttributes(state) {
|
||||
var menu = state.videoAccessibleMenu;
|
||||
|
||||
menu.button.attr({
|
||||
'role': 'button',
|
||||
'aria-disabled': 'false'
|
||||
});
|
||||
|
||||
menu.menuList.attr('role', 'menu');
|
||||
|
||||
menu.menuItemsLinks.each(function(){
|
||||
$(this).attr({
|
||||
'role': 'menuitem',
|
||||
'aria-disabled': 'false'
|
||||
/** Bind any necessary function callbacks to DOM events. */
|
||||
bindHandlers: function() {
|
||||
// Attach various events handlers to menu container.
|
||||
this.el.on({
|
||||
'mouseenter': this.openMenuHandler,
|
||||
'mouseleave': this.closeMenuHandler,
|
||||
'click': this.toggleMenuHandler,
|
||||
'keydown': this.keyDownHandler
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get previous element in array or cyles back to the last if it is the
|
||||
// first.
|
||||
function _previousMenuItemLink(links, index) {
|
||||
return $(links.eq(index < 1 ? links.length - 1 : index - 1));
|
||||
}
|
||||
// Attach click and keydown event handlers to individual menu items.
|
||||
this.menuItems
|
||||
.on('click', 'a.a11y-menu-item-link', this.clickHandler)
|
||||
.on('keydown', 'a.a11y-menu-item-link', this.keyDownHandler);
|
||||
},
|
||||
|
||||
// Get next element in array or cyles back to the first if it is the last.
|
||||
function _nextMenuItemLink(links, index) {
|
||||
return $(links.eq(index >= links.length - 1 ? 0 : index + 1));
|
||||
}
|
||||
// Get previous element in array or cyles back to the last if it is the
|
||||
// first.
|
||||
previousMenuItemLink: function(links, index) {
|
||||
return index < 1 ? links.last() : links.eq(index - 1);
|
||||
},
|
||||
|
||||
function _menuItemsLinksFocused(menu) {
|
||||
return menu.menuItemsLinks.is(':focus');
|
||||
}
|
||||
// Get next element in array or cyles back to the first if it is the last.
|
||||
nextMenuItemLink: function(links, index) {
|
||||
return index >= links.length - 1 ? links.first() : links.eq(index + 1);
|
||||
},
|
||||
|
||||
function _openMenu(menu, without_handler) {
|
||||
// When menu items have focus, the menu stays open on
|
||||
// mouseleave. A _closeMenuHandler is added to the window
|
||||
// element to have clicks close the menu when they happen
|
||||
// outside of it. We namespace the click event to easily remove it (and
|
||||
// only it) in _closeMenu.
|
||||
menu.container.addClass('open');
|
||||
menu.button.text('...');
|
||||
if (!without_handler) {
|
||||
$(window).on('click.currentMenu', _closeMenuHandler.bind(menu));
|
||||
}
|
||||
menuItemsLinksFocused: function() {
|
||||
return this.menuItemsLinks.is(':focus');
|
||||
},
|
||||
|
||||
// @TODO: onOpen callback
|
||||
}
|
||||
openMenu: function(withoutHandler) {
|
||||
// When menu items have focus, the menu stays open on
|
||||
// mouseleave. A closeMenuHandler is added to the window
|
||||
// element to have clicks close the menu when they happen
|
||||
// outside of it. We namespace the click event to easily remove it (and
|
||||
// only it) in closeMenu.
|
||||
this.el.addClass('open');
|
||||
this.button.text('...');
|
||||
if (!withoutHandler) {
|
||||
$(window).on('click.currentMenu', this.closeMenuHandler);
|
||||
}
|
||||
// @TODO: onOpen callback
|
||||
},
|
||||
|
||||
function _closeMenu(menu, without_handler) {
|
||||
// Remove the previously added clickHandler from window element.
|
||||
var msg = '.' + menu.value;
|
||||
closeMenu: function(withoutHandler) {
|
||||
// Remove the previously added clickHandler from window element.
|
||||
var msg = '.' + this.value;
|
||||
|
||||
menu.container.removeClass('open');
|
||||
menu.button.text(gettext(msg));
|
||||
if (!without_handler) {
|
||||
$(window).off('click.currentMenu');
|
||||
}
|
||||
this.el.removeClass('open');
|
||||
this.button.text(gettext(msg));
|
||||
if (!withoutHandler) {
|
||||
$(window).off('click.currentMenu');
|
||||
}
|
||||
// @TODO: onClose callback
|
||||
},
|
||||
|
||||
// @TODO: onClose callback
|
||||
}
|
||||
openMenuHandler: function() {
|
||||
this.openMenu(true);
|
||||
return false;
|
||||
},
|
||||
|
||||
function _openMenuHandler(event) {
|
||||
_openMenu(this, true);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function _closeMenuHandler(event) {
|
||||
// Only close the menu if no menu item link has focus or `click` event.
|
||||
if (!_menuItemsLinksFocused(this) || event.type == 'click') {
|
||||
_closeMenu(this, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function _toggleMenuHandler(event) {
|
||||
if (this.container.hasClass('open')) {
|
||||
_closeMenu(this, true);
|
||||
} else {
|
||||
_openMenu(this, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Various event handlers. They all return false to stop propagation and
|
||||
// prevent default behavior.
|
||||
function _clickHandler(event) {
|
||||
var target = $(event.currentTarget);
|
||||
|
||||
this.changeFileType.call(this, event);
|
||||
_closeMenu(this, true);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function _keyDownHandler(event) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode,
|
||||
target = $(event.currentTarget),
|
||||
index;
|
||||
|
||||
if (target.is('a.a11y-menu-item-link')) {
|
||||
|
||||
index = target.parent().index();
|
||||
|
||||
switch (keyCode) {
|
||||
// Scroll up menu, wrapping at the top. Keep menu open.
|
||||
case KEY.UP:
|
||||
_previousMenuItemLink(this.menuItemsLinks, index).focus();
|
||||
break;
|
||||
// Scroll down menu, wrapping at the bottom. Keep menu
|
||||
// open.
|
||||
case KEY.DOWN:
|
||||
_nextMenuItemLink(this.menuItemsLinks, index).focus();
|
||||
break;
|
||||
// Close menu.
|
||||
case KEY.TAB:
|
||||
_closeMenu(this);
|
||||
// TODO
|
||||
// What has to happen here? In speed menu, tabbing backward
|
||||
// will give focus to Play/Pause button and tabbing
|
||||
// forward to Volume button.
|
||||
break;
|
||||
// Close menu, give focus to button and change
|
||||
// file type.
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
this.button.focus();
|
||||
this.changeFileType.call(this, event);
|
||||
_closeMenu(this);
|
||||
break;
|
||||
// Close menu and give focus to speed control.
|
||||
case KEY.ESCAPE:
|
||||
_closeMenu(this);
|
||||
this.button.focus();
|
||||
break;
|
||||
closeMenuHandler: function(event) {
|
||||
// Only close the menu if no menu item link has focus or `click` event.
|
||||
if (!this.menuItemsLinksFocused() || event.type === 'click') {
|
||||
this.closeMenu(true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
switch(keyCode) {
|
||||
// Open menu and focus on last element of list above it.
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
case KEY.UP:
|
||||
_openMenu(this);
|
||||
this.menuItemsLinks.last().focus();
|
||||
break;
|
||||
// Close menu.
|
||||
case KEY.ESCAPE:
|
||||
_closeMenu(this);
|
||||
break;
|
||||
},
|
||||
|
||||
toggleMenuHandler: function() {
|
||||
if (this.el.hasClass('open')) {
|
||||
this.closeMenu(true);
|
||||
} else {
|
||||
this.openMenu(true);
|
||||
}
|
||||
// We do not stop propagation and default behavior on a TAB
|
||||
// keypress.
|
||||
return event.keyCode === KEY.TAB;
|
||||
return false;
|
||||
},
|
||||
|
||||
// Various event handlers. They all return false to stop propagation and
|
||||
// prevent default behavior.
|
||||
clickHandler: function(event) {
|
||||
this.changeFileType.call(this, event);
|
||||
this.closeMenu(true);
|
||||
return false;
|
||||
},
|
||||
|
||||
keyDownHandler: function(event) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode,
|
||||
target = $(event.currentTarget),
|
||||
index;
|
||||
|
||||
if (target.is('a.a11y-menu-item-link')) {
|
||||
index = target.parent().index();
|
||||
switch (keyCode) {
|
||||
// Scroll up menu, wrapping at the top. Keep menu open.
|
||||
case KEY.UP:
|
||||
this.previousMenuItemLink(this.menuItemsLinks, index).focus();
|
||||
break;
|
||||
// Scroll down menu, wrapping at the bottom. Keep menu
|
||||
// open.
|
||||
case KEY.DOWN:
|
||||
this.nextMenuItemLink(this.menuItemsLinks, index).focus();
|
||||
break;
|
||||
// Close menu.
|
||||
case KEY.TAB:
|
||||
this.closeMenu();
|
||||
// TODO
|
||||
// What has to happen here? In speed menu, tabbing backward
|
||||
// will give focus to Play/Pause button and tabbing
|
||||
// forward to Volume button.
|
||||
break;
|
||||
// Close menu, give focus to button and change
|
||||
// file type.
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
this.button.focus();
|
||||
this.changeFileType.call(this, event);
|
||||
this.closeMenu();
|
||||
break;
|
||||
// Close menu and give focus to speed control.
|
||||
case KEY.ESCAPE:
|
||||
this.closeMenu();
|
||||
this.button.focus();
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
switch(keyCode) {
|
||||
// Open menu and focus on last element of list above it.
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
case KEY.UP:
|
||||
this.openMenu();
|
||||
this.menuItemsLinks.last().focus();
|
||||
break;
|
||||
// Close menu.
|
||||
case KEY.ESCAPE:
|
||||
this.closeMenu();
|
||||
break;
|
||||
}
|
||||
// We do not stop propagation and default behavior on a TAB
|
||||
// keypress.
|
||||
return event.keyCode === KEY.TAB;
|
||||
}
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.value = value;
|
||||
this.menuItems
|
||||
.removeClass('active')
|
||||
.find("a[data-value='" + value + "']")
|
||||
.parent()
|
||||
.addClass('active');
|
||||
},
|
||||
|
||||
changeFileType: function(event) {
|
||||
var fileType = $(event.currentTarget).data('value'),
|
||||
data = {'transcript_download_format': fileType};
|
||||
|
||||
this.setValue(fileType);
|
||||
this.options.storage.setItem('transcript_download_format', fileType);
|
||||
|
||||
$.ajax({
|
||||
url: this.options.saveStateUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc Bind any necessary function callbacks to DOM events (click,
|
||||
* mousemove, etc.).
|
||||
*
|
||||
* @type {function}
|
||||
* @access private
|
||||
*
|
||||
* @param {object} state The object containg the state of the video player.
|
||||
* All other modules, their parameters, public variables, etc. are
|
||||
* available via this object.
|
||||
*
|
||||
* @this {object} The global window object.
|
||||
*
|
||||
* @returns {undefined}
|
||||
*/
|
||||
function _bindHandlers(state) {
|
||||
var menu = state.videoAccessibleMenu;
|
||||
|
||||
// Attach various events handlers to menu container.
|
||||
menu.container.on({
|
||||
'mouseenter': _openMenuHandler.bind(menu),
|
||||
'mouseleave': _closeMenuHandler.bind(menu),
|
||||
'click': _toggleMenuHandler.bind(menu),
|
||||
'keydown': _keyDownHandler.bind(menu)
|
||||
});
|
||||
|
||||
// Attach click and keydown event handlers to individual menu items.
|
||||
menu.menuItems
|
||||
.on('click', 'a.a11y-menu-item-link', _clickHandler.bind(menu))
|
||||
.on('keydown', 'a.a11y-menu-item-link', _keyDownHandler.bind(menu));
|
||||
}
|
||||
|
||||
function setValue(value) {
|
||||
var menu = this.videoAccessibleMenu;
|
||||
|
||||
menu.value = value;
|
||||
menu.menuItems
|
||||
.removeClass('active')
|
||||
.find("a[data-value='" + value + "']")
|
||||
.parent()
|
||||
.addClass('active');
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// 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 changeFileType(event) {
|
||||
var fileType = $(event.currentTarget).data('value');
|
||||
|
||||
this.videoAccessibleMenu.setValue(fileType);
|
||||
this.saveState(true, {'transcript_download_format': fileType});
|
||||
this.storage.setItem('transcript_download_format', fileType);
|
||||
}
|
||||
};
|
||||
|
||||
return VideoAccessibleMenu;
|
||||
});
|
||||
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
}(RequireJS.define));
|
||||
|
||||
@@ -15,6 +15,7 @@ function (HTML5Video, Resizer) {
|
||||
return dfd.promise();
|
||||
},
|
||||
methodsDict = {
|
||||
destroy: destroy,
|
||||
duration: duration,
|
||||
handlePlaybackQualityChange: handlePlaybackQualityChange,
|
||||
|
||||
@@ -28,13 +29,14 @@ function (HTML5Video, Resizer) {
|
||||
isEnded: isEnded,
|
||||
isPlaying: isPlaying,
|
||||
isUnstarted: isUnstarted,
|
||||
log: log,
|
||||
onCaptionSeek: onSeek,
|
||||
onEnded: onEnded,
|
||||
onError: onError,
|
||||
onPause: onPause,
|
||||
onPlay: onPlay,
|
||||
runTimer: runTimer,
|
||||
stopTimer: stopTimer,
|
||||
onLoadMetadataHtml5: onLoadMetadataHtml5,
|
||||
onPlaybackQualityChange: onPlaybackQualityChange,
|
||||
onReady: onReady,
|
||||
onSlideSeek: onSeek,
|
||||
@@ -49,8 +51,7 @@ function (HTML5Video, Resizer) {
|
||||
update: update,
|
||||
figureOutStartEndTime: figureOutStartEndTime,
|
||||
figureOutStartingTime: figureOutStartingTime,
|
||||
updatePlayTime: updatePlayTime,
|
||||
logStopVideo:logStopVideo
|
||||
updatePlayTime: updatePlayTime
|
||||
};
|
||||
|
||||
VideoPlayer.prototype = methodsDict;
|
||||
@@ -80,6 +81,17 @@ function (HTML5Video, Resizer) {
|
||||
state.videoPlayer.onCaptionSeek = debouncedF;
|
||||
}
|
||||
|
||||
// Updates players state, once metadata is loaded for html5 player.
|
||||
function onLoadMetadataHtml5() {
|
||||
var player = this.videoPlayer.player.videoEl,
|
||||
videoWidth = player[0].videoWidth || player.width(),
|
||||
videoHeight = player[0].videoHeight || player.height();
|
||||
|
||||
_resize(this, videoWidth, videoHeight);
|
||||
_updateVcrAndRegion(this);
|
||||
}
|
||||
|
||||
|
||||
// function _initialize(state)
|
||||
//
|
||||
// Create any necessary DOM elements, attach them, and set their
|
||||
@@ -94,8 +106,6 @@ function (HTML5Video, Resizer) {
|
||||
// metadata is loaded, which normally happens just after the video
|
||||
// starts playing. Just after that configurations can be applied.
|
||||
state.videoPlayer.ready = _.once(function () {
|
||||
$(window).on('unload', state.saveState);
|
||||
|
||||
if (!state.isFlashMode() && state.speed != '1.0') {
|
||||
|
||||
// Work around a bug in the Youtube API that causes videos to
|
||||
@@ -150,20 +160,13 @@ function (HTML5Video, Resizer) {
|
||||
videoSources: state.config.sources,
|
||||
events: {
|
||||
onReady: state.videoPlayer.onReady,
|
||||
onStateChange: state.videoPlayer.onStateChange
|
||||
onStateChange: state.videoPlayer.onStateChange,
|
||||
onError: state.videoPlayer.onError
|
||||
}
|
||||
});
|
||||
|
||||
player = state.videoEl = state.videoPlayer.player.videoEl;
|
||||
|
||||
player[0].addEventListener('loadedmetadata', function () {
|
||||
var videoWidth = player[0].videoWidth || player.width(),
|
||||
videoHeight = player[0].videoHeight || player.height();
|
||||
|
||||
_resize(state, videoWidth, videoHeight);
|
||||
|
||||
_updateVcrAndRegion(state);
|
||||
}, false);
|
||||
player[0].addEventListener('loadedmetadata', state.videoPlayer.onLoadMetadataHtml5, false);
|
||||
|
||||
} else {
|
||||
youTubeId = state.youtubeId();
|
||||
@@ -174,8 +177,8 @@ function (HTML5Video, Resizer) {
|
||||
events: {
|
||||
onReady: state.videoPlayer.onReady,
|
||||
onStateChange: state.videoPlayer.onStateChange,
|
||||
onPlaybackQualityChange: state.videoPlayer
|
||||
.onPlaybackQualityChange
|
||||
onPlaybackQualityChange: state.videoPlayer.onPlaybackQualityChange,
|
||||
onError: state.videoPlayer.onError
|
||||
}
|
||||
});
|
||||
|
||||
@@ -261,8 +264,8 @@ function (HTML5Video, Resizer) {
|
||||
});
|
||||
}
|
||||
|
||||
$(window).on('resize', _.debounce(function () {
|
||||
state.trigger('videoControl.updateControlsHeight', null);
|
||||
$(window).on('resize.video', _.debounce(function () {
|
||||
state.trigger('videoFullScreen.updateControlsHeight', null);
|
||||
state.el.trigger('caption:resize');
|
||||
state.resizer.align();
|
||||
}, 100));
|
||||
@@ -292,8 +295,8 @@ function (HTML5Video, Resizer) {
|
||||
events: {
|
||||
onReady: state.videoPlayer.onReady,
|
||||
onStateChange: state.videoPlayer.onStateChange,
|
||||
onPlaybackQualityChange: state.videoPlayer
|
||||
.onPlaybackQualityChange
|
||||
onPlaybackQualityChange: state.videoPlayer.onPlaybackQualityChange,
|
||||
onError: state.videoPlayer.onError
|
||||
}
|
||||
});
|
||||
|
||||
@@ -309,6 +312,28 @@ function (HTML5Video, Resizer) {
|
||||
// them available and sets up their context is makeFunctionsPublic().
|
||||
// ***************************************************************
|
||||
|
||||
function destroy() {
|
||||
var player = this.videoPlayer.player;
|
||||
this.el.removeClass([
|
||||
'is-unstarted', 'is-playing', 'is-paused', 'is-buffered',
|
||||
'is-ended', 'is-cued'
|
||||
].join(' '));
|
||||
$(window).off('.video');
|
||||
this.el.trigger('destroy');
|
||||
this.el.off();
|
||||
this.videoPlayer.stopTimer();
|
||||
if (this.resizer && this.resizer.destroy) {
|
||||
this.resizer.destroy();
|
||||
}
|
||||
if (player && player.video) {
|
||||
player.video.removeEventListener('loadedmetadata', this.videoPlayer.onLoadMetadataHtml5, false);
|
||||
}
|
||||
if (player && _.isFunction(player.destroy)) {
|
||||
player.destroy();
|
||||
}
|
||||
delete this.videoPlayer;
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if (this.videoPlayer.player.pauseVideo) {
|
||||
this.videoPlayer.player.pauseVideo();
|
||||
@@ -349,9 +374,10 @@ function (HTML5Video, Resizer) {
|
||||
this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
|
||||
end: true
|
||||
});
|
||||
// Emit `stop_video` event
|
||||
this.videoPlayer.logStopVideo();
|
||||
|
||||
this.el.trigger('stop');
|
||||
}
|
||||
this.el.trigger('timeupdate', [this.videoPlayer.currentTime]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,19 +462,8 @@ function (HTML5Video, Resizer) {
|
||||
}
|
||||
|
||||
newSpeed = parseFloat(newSpeed).toFixed(2).replace(/\.00$/, '.0');
|
||||
|
||||
this.videoPlayer.log(
|
||||
'speed_change_video',
|
||||
{
|
||||
current_time: time,
|
||||
old_speed: this.speed,
|
||||
new_speed: newSpeed
|
||||
}
|
||||
);
|
||||
|
||||
this.setSpeed(newSpeed, true);
|
||||
this.setSpeed(newSpeed);
|
||||
this.videoPlayer.setPlaybackRate(newSpeed);
|
||||
this.saveState(true, { speed: newSpeed });
|
||||
}
|
||||
|
||||
// Every 200 ms, if the video is playing, we call the function update, via
|
||||
@@ -459,20 +474,12 @@ function (HTML5Video, Resizer) {
|
||||
var time = params.time,
|
||||
type = params.type,
|
||||
oldTime = this.videoPlayer.currentTime;
|
||||
|
||||
// After the user seeks, the video will start playing from
|
||||
// the sought point, and stop playing at the end.
|
||||
this.videoPlayer.goToStartTime = false;
|
||||
|
||||
this.videoPlayer.seekTo(time);
|
||||
this.videoPlayer.log(
|
||||
'seek_video',
|
||||
{
|
||||
old_time: oldTime,
|
||||
new_time: time,
|
||||
type: type
|
||||
}
|
||||
);
|
||||
this.el.trigger('seek', [time, oldTime, type]);
|
||||
}
|
||||
|
||||
function seekTo(time) {
|
||||
@@ -509,7 +516,6 @@ function (HTML5Video, Resizer) {
|
||||
}
|
||||
|
||||
this.videoPlayer.updatePlayTime(time, true);
|
||||
this.el.trigger('seek', arguments);
|
||||
|
||||
// the timer is stopped above; restart it.
|
||||
if (this.videoPlayer.isPlaying()) {
|
||||
@@ -534,9 +540,8 @@ function (HTML5Video, Resizer) {
|
||||
|
||||
function onEnded() {
|
||||
var time = this.videoPlayer.duration();
|
||||
this.videoPlayer.logStopVideo();
|
||||
|
||||
this.trigger('videoControl.pause', null);
|
||||
|
||||
this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
|
||||
end: true
|
||||
});
|
||||
@@ -544,40 +549,20 @@ function (HTML5Video, Resizer) {
|
||||
if (this.videoPlayer.skipOnEndedStartEndReset) {
|
||||
this.videoPlayer.skipOnEndedStartEndReset = undefined;
|
||||
}
|
||||
|
||||
// Sometimes `onEnded` events fires when `currentTime` not equal
|
||||
// `duration`. In this case, slider doesn't reach the end point of
|
||||
// timeline.
|
||||
this.videoPlayer.updatePlayTime(time);
|
||||
|
||||
this.el.trigger('ended', arguments);
|
||||
}
|
||||
|
||||
function onPause() {
|
||||
this.videoPlayer.log(
|
||||
'pause_video',
|
||||
{
|
||||
currentTime: this.videoPlayer.currentTime
|
||||
}
|
||||
);
|
||||
|
||||
this.videoPlayer.stopTimer();
|
||||
|
||||
this.trigger('videoControl.pause', null);
|
||||
this.saveState(true);
|
||||
this.el.trigger('pause', arguments);
|
||||
}
|
||||
|
||||
function onPlay() {
|
||||
this.videoPlayer.log(
|
||||
'play_video',
|
||||
{
|
||||
currentTime: this.videoPlayer.currentTime
|
||||
}
|
||||
);
|
||||
|
||||
this.videoPlayer.runTimer();
|
||||
this.trigger('videoControl.play', null);
|
||||
this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
|
||||
end: false
|
||||
});
|
||||
@@ -591,22 +576,12 @@ function (HTML5Video, Resizer) {
|
||||
this.videoPlayer.player.setPlaybackQuality(value);
|
||||
}
|
||||
|
||||
function logStopVideo(){
|
||||
this.videoPlayer.log(
|
||||
'stop_video',
|
||||
{
|
||||
currentTime: this.videoPlayer.currentTime
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function onPlaybackQualityChange() {
|
||||
var quality;
|
||||
|
||||
quality = this.videoPlayer.player.getPlaybackQuality();
|
||||
|
||||
this.trigger('videoQualityControl.onQualityChange', quality);
|
||||
|
||||
this.el.trigger('qualitychange', arguments);
|
||||
}
|
||||
|
||||
@@ -625,8 +600,6 @@ function (HTML5Video, Resizer) {
|
||||
_this.videoPlayer.onVolumeChange(volume);
|
||||
});
|
||||
|
||||
this.videoPlayer.log('load_video');
|
||||
|
||||
availablePlaybackRates = this.videoPlayer.player
|
||||
.getAvailablePlaybackRates();
|
||||
|
||||
@@ -717,6 +690,10 @@ function (HTML5Video, Resizer) {
|
||||
}
|
||||
|
||||
this.el.trigger('ready', arguments);
|
||||
|
||||
if (this.config.autoplay) {
|
||||
this.videoPlayer.play();
|
||||
}
|
||||
}
|
||||
|
||||
function onStateChange(event) {
|
||||
@@ -755,6 +732,10 @@ function (HTML5Video, Resizer) {
|
||||
}
|
||||
}
|
||||
|
||||
function onError (code) {
|
||||
this.el.trigger('error', [code]);
|
||||
}
|
||||
|
||||
function figureOutStartEndTime(duration) {
|
||||
var videoPlayer = this.videoPlayer;
|
||||
|
||||
@@ -937,30 +918,6 @@ function (HTML5Video, Resizer) {
|
||||
return Math.floor(dur);
|
||||
}
|
||||
|
||||
function log(eventName, data) {
|
||||
var logInfo;
|
||||
|
||||
// Default parameters that always get logged.
|
||||
logInfo = {
|
||||
id: this.id
|
||||
};
|
||||
|
||||
// If extra parameters were passed to the log.
|
||||
if (data) {
|
||||
$.each(data, function (paramName, value) {
|
||||
logInfo[paramName] = value;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.isYoutubeType()) {
|
||||
logInfo.code = this.youtubeId();
|
||||
} else {
|
||||
logInfo.code = 'html5';
|
||||
}
|
||||
|
||||
Logger.log(eventName, logInfo);
|
||||
}
|
||||
|
||||
function onVolumeChange(volume) {
|
||||
this.videoPlayer.player.setVolume(volume);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
(function (requirejs, require, define) {
|
||||
|
||||
// VideoControl module.
|
||||
define(
|
||||
'video/04_video_control.js',
|
||||
@@ -30,24 +29,29 @@ function () {
|
||||
// get the 'state' object as a context.
|
||||
function _makeFunctionsPublic(state) {
|
||||
var methodsDict = {
|
||||
exitFullScreenHandler: exitFullScreenHandler,
|
||||
destroy: destroy,
|
||||
hideControls: hideControls,
|
||||
hidePlayPlaceholder: hidePlayPlaceholder,
|
||||
pause: pause,
|
||||
play: play,
|
||||
show: show,
|
||||
showControls: showControls,
|
||||
showPlayPlaceholder: showPlayPlaceholder,
|
||||
toggleFullScreen: toggleFullScreen,
|
||||
toggleFullScreenHandler: toggleFullScreenHandler,
|
||||
togglePlayback: togglePlayback,
|
||||
updateControlsHeight: updateControlsHeight,
|
||||
focusFirst: focusFirst,
|
||||
updateVcrVidTime: updateVcrVidTime
|
||||
};
|
||||
|
||||
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)
|
||||
//
|
||||
// Create any necessary DOM elements, attach them, and set their initial configuration. Also
|
||||
@@ -55,21 +59,7 @@ function () {
|
||||
// way - you don't have to do repeated jQuery element selects.
|
||||
function _renderElements(state) {
|
||||
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.fullScreenState = false;
|
||||
state.videoControl.pause();
|
||||
|
||||
if (state.isTouch && state.videoType === 'html5') {
|
||||
state.videoControl.showPlayPlaceholder();
|
||||
}
|
||||
state.videoControl.vidTimeEl = state.videoControl.el.find('.vidtime');
|
||||
|
||||
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
|
||||
state.videoControl.fadeOutTimeout = state.config.fadeOutTimeout;
|
||||
@@ -77,62 +67,23 @@ function () {
|
||||
state.videoControl.el.addClass('html5');
|
||||
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)
|
||||
//
|
||||
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
|
||||
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)) {
|
||||
state.el.on('mousemove', state.videoControl.showControls);
|
||||
state.el.on('keydown', state.videoControl.showControls);
|
||||
state.el.on({
|
||||
'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) {
|
||||
return control.el.height() + 0.5 * control.sliderEl.height();
|
||||
state.el.on('destroy', state.videoControl.destroy);
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
@@ -141,10 +92,8 @@ function () {
|
||||
// The magic private function that makes them available and sets up their context is makeFunctionsPublic().
|
||||
// ***************************************************************
|
||||
|
||||
function updateControlsHeight () {
|
||||
this.videoControl.height = _getControlsHeight(this.videoControl);
|
||||
|
||||
return this.videoControl.height;
|
||||
function focusFirst() {
|
||||
this.videoControl.el.find('.vcr a, .vcr button').first().focus();
|
||||
}
|
||||
|
||||
function show() {
|
||||
@@ -171,13 +120,12 @@ function () {
|
||||
}
|
||||
|
||||
this.controlHideTimeout = setTimeout(this.videoControl.hideControls, this.videoControl.fadeOutTimeout);
|
||||
|
||||
this.controlShowLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
var _this;
|
||||
var _this = this;
|
||||
|
||||
this.controlHideTimeout = null;
|
||||
|
||||
@@ -186,12 +134,8 @@ function () {
|
||||
}
|
||||
|
||||
this.controlState = 'hiding';
|
||||
|
||||
_this = this;
|
||||
|
||||
this.videoControl.el.fadeOut(this.videoControl.fadeOutTimeout, function () {
|
||||
_this.controlState = 'invisible';
|
||||
|
||||
// If the focus was on the video control or the volume control,
|
||||
// then we must make sure to close these dialogs. Otherwise, after
|
||||
// next autofocus, these dialogs will be open, but the focus will
|
||||
@@ -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) {
|
||||
var endTime = (this.config.endTime !== null) ? this.config.endTime : params.duration;
|
||||
// in case endTime is accidentally specified as being greater than the video
|
||||
|
||||
175
common/lib/xmodule/xmodule/js/src/video/04_video_full_screen.js
Normal file
175
common/lib/xmodule/xmodule/js/src/video/04_video_full_screen.js
Normal file
@@ -0,0 +1,175 @@
|
||||
(function (define) {
|
||||
'use strict';
|
||||
define('video/04_video_full_screen.js', [], function () {
|
||||
var template = [
|
||||
'<a href="#" class="add-fullscreen" title="',
|
||||
gettext('Fill browser'), '" role="button" aria-disabled="false">',
|
||||
gettext('Fill browser'),
|
||||
'</a>'
|
||||
].join('');
|
||||
|
||||
// VideoControl() function - what this module "exports".
|
||||
return function (state) {
|
||||
var dfd = $.Deferred();
|
||||
|
||||
state.videoFullScreen = {};
|
||||
|
||||
_makeFunctionsPublic(state);
|
||||
_renderElements(state);
|
||||
_bindHandlers(state);
|
||||
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
|
||||
// function _makeFunctionsPublic(state)
|
||||
//
|
||||
// Functions which will be accessible via 'state' object. When called, these functions will
|
||||
// get the 'state' object as a context.
|
||||
function _makeFunctionsPublic(state) {
|
||||
var methodsDict = {
|
||||
destroy: destroy,
|
||||
enter: enter,
|
||||
exitHandler: exitHandler,
|
||||
exit: exit,
|
||||
onFullscreenChange: onFullscreenChange,
|
||||
toggle: toggle,
|
||||
toggleHandler: toggleHandler,
|
||||
updateControlsHeight: updateControlsHeight
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.videoFullScreen, state);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
$(document).off('keyup', this.videoFullScreen.exitHandler);
|
||||
this.videoFullScreen.fullScreenEl.remove();
|
||||
this.el.off({
|
||||
'fullscreen': this.videoFullScreen.onFullscreenChange,
|
||||
'destroy': this.videoFullScreen.destroy
|
||||
});
|
||||
if (this.isFullScreen) {
|
||||
this.videoFullScreen.exit();
|
||||
}
|
||||
delete this.videoFullScreen;
|
||||
}
|
||||
|
||||
// function _renderElements(state)
|
||||
//
|
||||
// Create any necessary DOM elements, attach them, and set their initial configuration. Also
|
||||
// make the created DOM elements available via the 'state' object. Much easier to work this
|
||||
// way - you don't have to do repeated jQuery element selects.
|
||||
function _renderElements(state) {
|
||||
state.videoFullScreen.fullScreenEl = $(template);
|
||||
state.videoFullScreen.sliderEl = state.el.find('.slider');
|
||||
state.videoFullScreen.fullScreenState = false;
|
||||
state.el.find('.secondary-controls').append(state.videoFullScreen.fullScreenEl);
|
||||
state.videoFullScreen.updateControlsHeight();
|
||||
}
|
||||
|
||||
// function _bindHandlers(state)
|
||||
//
|
||||
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
|
||||
function _bindHandlers(state) {
|
||||
state.videoFullScreen.fullScreenEl.on('click', state.videoFullScreen.toggleHandler);
|
||||
state.el.on({
|
||||
'fullscreen': state.videoFullScreen.onFullscreenChange,
|
||||
'destroy': state.videoFullScreen.destroy
|
||||
});
|
||||
$(document).on('keyup', state.videoFullScreen.exitHandler);
|
||||
}
|
||||
|
||||
function _getControlsHeight(controls, slider) {
|
||||
return controls.height() + 0.5 * slider.height();
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// Public functions start here.
|
||||
// These are available via the 'state' object. Their context ('this' keyword) is the 'state' object.
|
||||
// The magic private function that makes them available and sets up their context is makeFunctionsPublic().
|
||||
// ***************************************************************
|
||||
|
||||
function onFullscreenChange (event, isFullScreen) {
|
||||
var height = this.videoFullScreen.updateControlsHeight();
|
||||
|
||||
if (isFullScreen) {
|
||||
this.resizer
|
||||
.delta
|
||||
.substract(height, 'height')
|
||||
.setMode('both');
|
||||
|
||||
} else {
|
||||
this.resizer
|
||||
.delta
|
||||
.reset()
|
||||
.setMode('width');
|
||||
}
|
||||
}
|
||||
|
||||
function updateControlsHeight() {
|
||||
var controls = this.el.find('.video-controls'),
|
||||
slider = this.videoFullScreen.sliderEl;
|
||||
this.videoFullScreen.height = _getControlsHeight(controls, slider);
|
||||
return this.videoFullScreen.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler to toggle fullscreen mode.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
function toggleHandler(event) {
|
||||
event.preventDefault();
|
||||
this.videoCommands.execute('toggleFullScreen');
|
||||
}
|
||||
|
||||
function exit() {
|
||||
var fullScreenClassNameEl = this.el.add(document.documentElement);
|
||||
|
||||
this.videoFullScreen.fullScreenState = this.isFullScreen = false;
|
||||
fullScreenClassNameEl.removeClass('video-fullscreen');
|
||||
$(window).scrollTop(this.scrollPos);
|
||||
this.videoFullScreen.fullScreenEl
|
||||
.attr('title', gettext('Fill browser'))
|
||||
.text(gettext('Fill browser'));
|
||||
this.el.trigger('fullscreen', [this.isFullScreen]);
|
||||
}
|
||||
|
||||
function enter() {
|
||||
var fullScreenClassNameEl = this.el.add(document.documentElement);
|
||||
|
||||
this.scrollPos = $(window).scrollTop();
|
||||
$(window).scrollTop(0);
|
||||
this.videoFullScreen.fullScreenState = this.isFullScreen = true;
|
||||
fullScreenClassNameEl.addClass('video-fullscreen');
|
||||
this.videoFullScreen.fullScreenEl
|
||||
.attr('title', gettext('Exit full browser'))
|
||||
.text(gettext('Exit full browser'));
|
||||
this.el.trigger('fullscreen', [this.isFullScreen]);
|
||||
}
|
||||
|
||||
/** Toggle fullscreen mode. */
|
||||
function toggle() {
|
||||
if (this.videoFullScreen.fullScreenState) {
|
||||
this.videoFullScreen.exit();
|
||||
} else {
|
||||
this.videoFullScreen.enter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler to exit from fullscreen mode.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
function exitHandler(event) {
|
||||
if ((this.isFullScreen) && (event.keyCode === 27)) {
|
||||
event.preventDefault();
|
||||
this.videoCommands.execute('toggleFullScreen');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}(RequireJS.define));
|
||||
@@ -5,6 +5,12 @@ define(
|
||||
'video/05_video_quality_control.js',
|
||||
[],
|
||||
function () {
|
||||
var template = [
|
||||
'<a href="#" class="quality-control is-hidden" title="',
|
||||
gettext('HD off'), '" role="button" aria-disabled="false">',
|
||||
gettext('HD off'),
|
||||
'</a>'
|
||||
].join('');
|
||||
|
||||
// VideoQualityControl() function - what this module "exports".
|
||||
return function (state) {
|
||||
@@ -12,7 +18,6 @@ function () {
|
||||
|
||||
// Changing quality for now only works for YouTube videos.
|
||||
if (state.videoType !== 'youtube') {
|
||||
state.el.find('a.quality-control').remove();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -36,6 +41,7 @@ function () {
|
||||
// get the 'state' object as a context.
|
||||
function _makeFunctionsPublic(state) {
|
||||
var methodsDict = {
|
||||
destroy: destroy,
|
||||
fetchAvailableQualities: fetchAvailableQualities,
|
||||
onQualityChange: onQualityChange,
|
||||
showQualityControl: showQualityControl,
|
||||
@@ -45,16 +51,25 @@ function () {
|
||||
state.bindTo(methodsDict, state.videoQualityControl, state);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
this.videoQualityControl.el.off({
|
||||
'click': this.videoQualityControl.toggleQuality,
|
||||
'destroy': this.videoQualityControl.destroy
|
||||
});
|
||||
this.el.off('.quality');
|
||||
this.videoQualityControl.el.remove();
|
||||
delete this.videoQualityControl;
|
||||
}
|
||||
|
||||
// function _renderElements(state)
|
||||
//
|
||||
// Create any necessary DOM elements, attach them, and set their initial configuration. Also
|
||||
// make the created DOM elements available via the 'state' object. Much easier to work this
|
||||
// way - you don't have to do repeated jQuery element selects.
|
||||
function _renderElements(state) {
|
||||
state.videoQualityControl.el = state.el.find('a.quality-control');
|
||||
|
||||
state.videoQualityControl.el.show();
|
||||
var element = state.videoQualityControl.el = $(template);
|
||||
state.videoQualityControl.quality = 'large';
|
||||
state.el.find('.secondary-controls').append(element);
|
||||
}
|
||||
|
||||
// function _bindHandlers(state)
|
||||
@@ -64,9 +79,11 @@ function () {
|
||||
state.videoQualityControl.el.on('click',
|
||||
state.videoQualityControl.toggleQuality
|
||||
);
|
||||
state.el.on('play', _.once(
|
||||
state.el.on('play.quality', _.once(
|
||||
state.videoQualityControl.fetchAvailableQualities
|
||||
));
|
||||
|
||||
state.el.on('destroy.quality', state.videoQualityControl.destroy);
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
@@ -141,7 +158,7 @@ function () {
|
||||
event.preventDefault();
|
||||
|
||||
newQuality = isHD ? 'large' : 'highres';
|
||||
|
||||
|
||||
this.trigger('videoPlayer.handlePlaybackQualityChange', newQuality);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,15 +12,17 @@ define(
|
||||
'video/06_video_progress_slider.js',
|
||||
[],
|
||||
function () {
|
||||
var template = [
|
||||
'<div class="slider" title="', gettext('Video position'), '"></div>'
|
||||
].join('');
|
||||
|
||||
// VideoProgressSlider() function - what this module "exports".
|
||||
return function (state) {
|
||||
var dfd = $.Deferred();
|
||||
|
||||
state.videoProgressSlider = {};
|
||||
|
||||
_makeFunctionsPublic(state);
|
||||
_renderElements(state);
|
||||
// No callbacks to DOM events (click, mousemove, etc.).
|
||||
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
@@ -36,6 +38,7 @@ function () {
|
||||
// these functions will get the 'state' object as a context.
|
||||
function _makeFunctionsPublic(state) {
|
||||
var methodsDict = {
|
||||
destroy: destroy,
|
||||
buildSlider: buildSlider,
|
||||
getRangeParams: getRangeParams,
|
||||
onSlide: onSlide,
|
||||
@@ -49,6 +52,12 @@ function () {
|
||||
state.bindTo(methodsDict, state.videoProgressSlider, state);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
this.videoProgressSlider.el.removeAttr('tabindex').slider('destroy');
|
||||
this.el.off('destroy', this.videoProgressSlider.destroy);
|
||||
delete this.videoProgressSlider;
|
||||
}
|
||||
|
||||
// function _renderElements(state)
|
||||
//
|
||||
// Create any necessary DOM elements, attach them, and set their
|
||||
@@ -56,8 +65,9 @@ function () {
|
||||
// via the 'state' object. Much easier to work this way - you don't
|
||||
// have to do repeated jQuery element selects.
|
||||
function _renderElements(state) {
|
||||
state.videoProgressSlider.el = state.videoControl.sliderEl;
|
||||
state.videoProgressSlider.el = $(template);
|
||||
|
||||
state.el.find('.video-controls').prepend(state.videoProgressSlider.el);
|
||||
state.videoProgressSlider.buildSlider();
|
||||
_buildHandle(state);
|
||||
}
|
||||
@@ -81,6 +91,8 @@ function () {
|
||||
'aria-valuemin': '0',
|
||||
'aria-valuenow': state.videoPlayer.currentTime
|
||||
});
|
||||
|
||||
state.el.on('destroy', state.videoProgressSlider.destroy);
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
@@ -109,7 +121,7 @@ function () {
|
||||
// whole slider). Remember that endTime === null means the end-time
|
||||
// is set to the end of video by default.
|
||||
function updateStartEndTimeRegion(params) {
|
||||
var left, width, start, end, duration, rangeParams;
|
||||
var start, end, duration, rangeParams;
|
||||
|
||||
// We must have a duration in order to determine the area of range.
|
||||
// It also must be non-zero.
|
||||
|
||||
@@ -17,6 +17,10 @@ function() {
|
||||
return new VolumeControl(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'keyDownHandler', 'updateVolumeSilently',
|
||||
'onVolumeChangeHandler', 'openMenu', 'closeMenu',
|
||||
'toggleMuteHandler', 'keyDownButtonHandler', 'destroy'
|
||||
);
|
||||
this.state = state;
|
||||
this.state.videoVolumeControl = this;
|
||||
this.i18n = i18n;
|
||||
@@ -33,17 +37,55 @@ function() {
|
||||
/** Step to increase/decrease volume level via keyboard. */
|
||||
step: 20,
|
||||
|
||||
template: [
|
||||
'<div class="volume">',
|
||||
'<a href="#" role="button" aria-disabled="false" title="',
|
||||
gettext('Volume'), '" aria-label="',
|
||||
gettext('Click on this button to mute or unmute this video or press UP or DOWN buttons to increase or decrease volume level.'),
|
||||
'"></a>',
|
||||
'<div role="presentation" class="volume-slider-container">',
|
||||
'<div class="volume-slider"></div>',
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join(''),
|
||||
|
||||
destroy: function () {
|
||||
this.volumeSlider.slider('destroy');
|
||||
this.state.el.find('iframe').removeAttr('tabindex');
|
||||
this.a11y.destroy();
|
||||
this.cookie = this.a11y = null;
|
||||
this.closeMenu();
|
||||
|
||||
this.state.el
|
||||
.off('play.volume')
|
||||
.off({
|
||||
'keydown': this.keyDownHandler,
|
||||
'volumechange': this.onVolumeChangeHandler
|
||||
});
|
||||
this.el.off({
|
||||
'mouseenter': this.openMenu,
|
||||
'mouseleave': this.closeMenu
|
||||
});
|
||||
this.button.off({
|
||||
'mousedown': this.toggleMuteHandler,
|
||||
'keydown': this.keyDownButtonHandler,
|
||||
'focus': this.openMenu,
|
||||
'blur': this.closeMenu
|
||||
});
|
||||
this.el.remove();
|
||||
delete this.state.videoVolumeControl;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
var volume;
|
||||
|
||||
this.el = this.state.el.find('.volume');
|
||||
|
||||
if (this.state.isTouch) {
|
||||
// iOS doesn't support volume change
|
||||
this.el.remove();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.el = $(this.template);
|
||||
// Youtube iframe react on key buttons and has his own handlers.
|
||||
// So, we disallow focusing on iframe.
|
||||
this.state.el.find('iframe').attr('tabindex', -1);
|
||||
@@ -80,26 +122,28 @@ function() {
|
||||
// Therefore, we do not need redundant focusing on slider in TAB
|
||||
// order.
|
||||
container.find('a').attr('tabindex', -1);
|
||||
this.state.el.find('.secondary-controls').append(this.el);
|
||||
},
|
||||
|
||||
/** Bind any necessary function callbacks to DOM events. */
|
||||
bindHandlers: function() {
|
||||
this.state.el.on({
|
||||
'keydown': this.keyDownHandler.bind(this),
|
||||
'play': _.once(this.updateVolumeSilently.bind(this)),
|
||||
'volumechange': this.onVolumeChangeHandler.bind(this)
|
||||
'keydown': this.keyDownHandler,
|
||||
'play.volume': _.once(this.updateVolumeSilently),
|
||||
'volumechange': this.onVolumeChangeHandler
|
||||
});
|
||||
this.el.on({
|
||||
'mouseenter': this.openMenu.bind(this),
|
||||
'mouseleave': this.closeMenu.bind(this)
|
||||
'mouseenter': this.openMenu,
|
||||
'mouseleave': this.closeMenu
|
||||
});
|
||||
this.button.on({
|
||||
'click': false,
|
||||
'mousedown': this.toggleMuteHandler.bind(this),
|
||||
'keydown': this.keyDownButtonHandler.bind(this),
|
||||
'focus': this.openMenu.bind(this),
|
||||
'blur': this.closeMenu.bind(this)
|
||||
'mousedown': this.toggleMuteHandler,
|
||||
'keydown': this.keyDownButtonHandler,
|
||||
'focus': this.openMenu,
|
||||
'blur': this.closeMenu
|
||||
});
|
||||
this.state.el.on('destroy', this.destroy);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -343,6 +387,10 @@ function() {
|
||||
};
|
||||
|
||||
Accessibility.prototype = {
|
||||
destroy: function () {
|
||||
this.liveRegion.remove();
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
this.liveRegion = $('<div />', {
|
||||
|
||||
@@ -16,6 +16,10 @@ function (Iterator) {
|
||||
return new SpeedControl(state);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onSetSpeed', 'onRenderSpeed', 'clickLinkHandler',
|
||||
'keyDownLinkHandler', 'mouseEnterHandler', 'mouseLeaveHandler',
|
||||
'clickMenuHandler', 'keyDownMenuHandler', 'destroy'
|
||||
);
|
||||
this.state = state;
|
||||
this.state.videoSpeedControl = this;
|
||||
this.initialize();
|
||||
@@ -24,24 +28,51 @@ function (Iterator) {
|
||||
};
|
||||
|
||||
SpeedControl.prototype = {
|
||||
template: [
|
||||
'<div class="speeds menu-container">',
|
||||
'<a class="speed-button" href="#" title="',
|
||||
gettext('Speeds'), '" role="button" aria-disabled="false">',
|
||||
'<span class="label">', gettext('Speed'), '</span>',
|
||||
'<span class="value"></span>',
|
||||
'</a>',
|
||||
'<ol class="video-speeds menu" role="menu"></ol>',
|
||||
'</div>'
|
||||
].join(''),
|
||||
|
||||
destroy: function () {
|
||||
this.el.off({
|
||||
'mouseenter': this.mouseEnterHandler,
|
||||
'mouseleave': this.mouseLeaveHandler,
|
||||
'click': this.clickMenuHandler,
|
||||
'keydown': this.keyDownMenuHandler
|
||||
});
|
||||
|
||||
this.state.el.off({
|
||||
'speed:set': this.onSetSpeed,
|
||||
'speed:render': this.onRenderSpeed
|
||||
});
|
||||
this.closeMenu(true);
|
||||
this.speedsContainer.remove();
|
||||
this.el.remove();
|
||||
delete this.state.videoSpeedControl;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function () {
|
||||
var state = this.state;
|
||||
|
||||
this.el = state.el.find('.speeds');
|
||||
this.speedsContainer = this.el.find('.video-speeds');
|
||||
this.speedButton = this.el.find('.speed-button');
|
||||
|
||||
if (!this.isPlaybackRatesSupported(state)) {
|
||||
this.el.remove();
|
||||
console.log(
|
||||
'[Video info]: playbackRate is not supported.'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this.el = $(this.template);
|
||||
this.speedsContainer = this.el.find('.video-speeds');
|
||||
this.speedButton = this.el.find('.speed-button');
|
||||
this.render(state.speeds, state.speed);
|
||||
this.setSpeed(state.speed, true, true);
|
||||
this.bindHandlers();
|
||||
|
||||
return true;
|
||||
@@ -51,13 +82,11 @@ function (Iterator) {
|
||||
* Creates any necessary DOM elements, attach them, and set their,
|
||||
* initial configuration.
|
||||
* @param {array} speeds List of speeds available for the player.
|
||||
* @param {string|number} currentSpeed Current speed for the player.
|
||||
*/
|
||||
render: function (speeds, currentSpeed) {
|
||||
var self = this,
|
||||
speedsContainer = this.speedsContainer,
|
||||
render: function (speeds) {
|
||||
var speedsContainer = this.speedsContainer,
|
||||
reversedSpeeds = speeds.concat().reverse(),
|
||||
speedsList = $.map(reversedSpeeds, function (speed, index) {
|
||||
speedsList = $.map(reversedSpeeds, function (speed) {
|
||||
return [
|
||||
'<li data-speed="', speed, '" role="presentation">',
|
||||
'<a class="speed-link" href="#" role="menuitem" tabindex="-1">',
|
||||
@@ -69,7 +98,7 @@ function (Iterator) {
|
||||
|
||||
speedsContainer.html(speedsList.join(''));
|
||||
this.speedLinks = new Iterator(speedsContainer.find('.speed-link'));
|
||||
this.setSpeed(currentSpeed, true, true);
|
||||
this.state.el.find('.secondary-controls').prepend(this.el);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -77,31 +106,34 @@ function (Iterator) {
|
||||
* mousemove, etc.).
|
||||
*/
|
||||
bindHandlers: function () {
|
||||
var self = this;
|
||||
|
||||
// Attach various events handlers to the speed menu button.
|
||||
this.el.on({
|
||||
'mouseenter': this.mouseEnterHandler.bind(this),
|
||||
'mouseleave': this.mouseLeaveHandler.bind(this),
|
||||
'click': this.clickMenuHandler.bind(this),
|
||||
'keydown': this.keyDownMenuHandler.bind(this)
|
||||
'mouseenter': this.mouseEnterHandler,
|
||||
'mouseleave': this.mouseLeaveHandler,
|
||||
'click': this.clickMenuHandler,
|
||||
'keydown': this.keyDownMenuHandler
|
||||
});
|
||||
|
||||
// Attach click and keydown event handlers to the individual speed
|
||||
// entries.
|
||||
this.speedsContainer.on({
|
||||
click: this.clickLinkHandler.bind(this),
|
||||
keydown: this.keyDownLinkHandler.bind(this)
|
||||
click: this.clickLinkHandler,
|
||||
keydown: this.keyDownLinkHandler
|
||||
}, 'a.speed-link');
|
||||
|
||||
this.state.el.on({
|
||||
'speed:set': function (event, speed) {
|
||||
self.setSpeed(speed, true);
|
||||
},
|
||||
'speed:render': function (event, speeds, currentSpeed) {
|
||||
self.render(speeds, currentSpeed);
|
||||
}
|
||||
'speed:set': this.onSetSpeed,
|
||||
'speed:render': this.onRenderSpeed
|
||||
});
|
||||
this.state.el.on('destroy', this.destroy);
|
||||
},
|
||||
|
||||
onSetSpeed: function (event, speed) {
|
||||
this.setSpeed(speed, true);
|
||||
},
|
||||
|
||||
onRenderSpeed: function (event, speeds, currentSpeed) {
|
||||
this.render(speeds, currentSpeed);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -133,7 +165,7 @@ function (Iterator) {
|
||||
// element to have clicks close the menu when they happen
|
||||
// outside of it.
|
||||
if (bindEvent) {
|
||||
$(window).on('click.speedMenu', this.clickMenuHandler.bind(this));
|
||||
$(window).on('click.speedMenu', this.clickMenuHandler);
|
||||
}
|
||||
|
||||
this.el.addClass('is-opened');
|
||||
@@ -175,7 +207,7 @@ function (Iterator) {
|
||||
this.currentSpeed = speed;
|
||||
|
||||
if (!silent) {
|
||||
this.el.trigger('speedchange', [speed]);
|
||||
this.el.trigger('speedchange', [speed, this.state.speed]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -656,6 +656,12 @@ function (Component) {
|
||||
|
||||
if (!state.isYoutubeType()) {
|
||||
state.el.find('video').contextmenu(state.el, options);
|
||||
state.el.on('destroy', function () {
|
||||
var contextmenu = $(this).find('video').data('contextmenu');
|
||||
if (contextmenu) {
|
||||
contextmenu.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
|
||||
109
common/lib/xmodule/xmodule/js/src/video/09_bumper.js
Normal file
109
common/lib/xmodule/xmodule/js/src/video/09_bumper.js
Normal file
@@ -0,0 +1,109 @@
|
||||
(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));
|
||||
@@ -0,0 +1,112 @@
|
||||
(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));
|
||||
129
common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js
Normal file
129
common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js
Normal file
@@ -0,0 +1,129 @@
|
||||
(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));
|
||||
@@ -0,0 +1,87 @@
|
||||
(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));
|
||||
@@ -0,0 +1,87 @@
|
||||
(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));
|
||||
@@ -0,0 +1,84 @@
|
||||
(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));
|
||||
66
common/lib/xmodule/xmodule/js/src/video/09_poster.js
Normal file
66
common/lib/xmodule/xmodule/js/src/video/09_poster.js
Normal file
@@ -0,0 +1,66 @@
|
||||
(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));
|
||||
118
common/lib/xmodule/xmodule/js/src/video/09_save_state_plugin.js
Normal file
118
common/lib/xmodule/xmodule/js/src/video/09_save_state_plugin.js
Normal file
@@ -0,0 +1,118 @@
|
||||
(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));
|
||||
74
common/lib/xmodule/xmodule/js/src/video/09_skip_control.js
Normal file
74
common/lib/xmodule/xmodule/js/src/video/09_skip_control.js
Normal file
@@ -0,0 +1,74 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
// VideoSkipControl module.
|
||||
define(
|
||||
'video/09_skip_control.js', [],
|
||||
function() {
|
||||
/**
|
||||
* Video skip control module.
|
||||
* @exports video/09_skip_control.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
var SkipControl = function(state, i18n) {
|
||||
if (!(this instanceof SkipControl)) {
|
||||
return new SkipControl(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onClick', 'render', 'destroy');
|
||||
this.state = state;
|
||||
this.state.videoSkipControl = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
SkipControl.prototype = {
|
||||
template: [
|
||||
'<a class="video_control skip skip-control" href="#" title="',
|
||||
gettext('Do not show again'), '" role="button" aria-disabled="false">',
|
||||
gettext('Do not show again'),
|
||||
'</a>'
|
||||
].join(''),
|
||||
|
||||
destroy: function () {
|
||||
this.el.remove();
|
||||
this.state.el.off('.skip');
|
||||
delete this.state.videoSkipControl;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
this.el = $(this.template);
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates any necessary DOM elements, attach them, and set their,
|
||||
* initial configuration.
|
||||
*/
|
||||
render: function() {
|
||||
this.state.el.find('.vcr a').after(this.el);
|
||||
},
|
||||
|
||||
/** Bind any necessary function callbacks to DOM events. */
|
||||
bindHandlers: function() {
|
||||
this.el.on('click', this.onClick);
|
||||
this.state.el.on({
|
||||
'play.skip': _.once(this.render),
|
||||
'destroy.skip': this.destroy
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function (event) {
|
||||
event.preventDefault();
|
||||
this.state.videoCommands.execute('skip', true);
|
||||
}
|
||||
};
|
||||
|
||||
return SkipControl;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,5 +1,4 @@
|
||||
(function (define) {
|
||||
|
||||
// VideoCaption module.
|
||||
define(
|
||||
'video/09_video_caption.js',
|
||||
@@ -24,6 +23,10 @@ function (Sjson, AsyncProcess) {
|
||||
return new VideoCaption(state);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'toggle', 'onMouseEnter', 'onMouseLeave', 'onMovement',
|
||||
'onContainerMouseEnter', 'onContainerMouseLeave', 'fetchCaption',
|
||||
'onResize', 'pause', 'play', 'onCaptionUpdate', 'onCaptionHandler', 'destroy'
|
||||
);
|
||||
this.state = state;
|
||||
this.state.videoCaption = this;
|
||||
this.renderElements();
|
||||
@@ -32,29 +35,61 @@ function (Sjson, AsyncProcess) {
|
||||
};
|
||||
|
||||
VideoCaption.prototype = {
|
||||
langTemplate: [
|
||||
'<div class="lang menu-container">',
|
||||
'<a href="#" class="hide-subtitles" title="',
|
||||
gettext('Turn off captions'), '" role="button" aria-disabled="false">',
|
||||
gettext('Turn off captions'),
|
||||
'</a>',
|
||||
'</div>'
|
||||
].join(''),
|
||||
|
||||
template: [
|
||||
'<ol id="transcript-captions" class="subtitles" tabindex="0" role="group" aria-label="',
|
||||
gettext('Activating an item in this group will spool the video to the corresponding time point. To skip transcript, go to previous item.'),
|
||||
'">',
|
||||
'<li></li>',
|
||||
'</ol>'
|
||||
].join(''),
|
||||
|
||||
destroy: function () {
|
||||
this.state.el
|
||||
.off({
|
||||
'caption:fetch': this.fetchCaption,
|
||||
'caption:resize': this.onResize,
|
||||
'caption:update': this.onCaptionUpdate,
|
||||
'ended': this.pause,
|
||||
'fullscreen': this.onResize,
|
||||
'pause': this.pause,
|
||||
'play': this.play,
|
||||
'destroy': this.destroy
|
||||
})
|
||||
.removeClass('is-captions-rendered');
|
||||
if (this.fetchXHR && this.fetchXHR.abort) {
|
||||
this.fetchXHR.abort();
|
||||
}
|
||||
if (this.availableTranslationsXHR && this.availableTranslationsXHR.abort) {
|
||||
this.availableTranslationsXHR.abort();
|
||||
}
|
||||
this.subtitlesEl.remove();
|
||||
this.container.remove();
|
||||
delete this.state.videoCaption;
|
||||
},
|
||||
/**
|
||||
* @desc Initiate rendering of elements, and set their initial configuration.
|
||||
*
|
||||
*/
|
||||
renderElements: function () {
|
||||
var state = this.state,
|
||||
languages = this.state.config.transcriptLanguages;
|
||||
var languages = this.state.config.transcriptLanguages;
|
||||
|
||||
this.loaded = false;
|
||||
this.subtitlesEl = state.el.find('ol.subtitles');
|
||||
this.container = state.el.find('.lang');
|
||||
this.hideSubtitlesEl = state.el.find('a.hide-subtitles');
|
||||
this.subtitlesEl = $(this.template);
|
||||
this.container = $(this.langTemplate);
|
||||
this.hideSubtitlesEl = this.container.find('a.hide-subtitles');
|
||||
|
||||
if (_.keys(languages).length) {
|
||||
this.renderLanguageMenu(languages);
|
||||
|
||||
if (!this.fetchCaption()) {
|
||||
this.hideCaptions(true);
|
||||
this.hideSubtitlesEl.hide();
|
||||
}
|
||||
} else {
|
||||
this.hideCaptions(true, false);
|
||||
this.hideSubtitlesEl.hide();
|
||||
this.fetchCaption();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -64,65 +99,40 @@ function (Sjson, AsyncProcess) {
|
||||
*
|
||||
*/
|
||||
bindHandlers: function () {
|
||||
var self = this,
|
||||
state = this.state,
|
||||
var state = this.state,
|
||||
events = [
|
||||
'mouseover', 'mouseout', 'mousedown', 'click', 'focus', 'blur',
|
||||
'keydown'
|
||||
].join(' ');
|
||||
|
||||
// Change context to VideoCaption of event handlers using `bind`.
|
||||
this.hideSubtitlesEl.on('click', this.toggle.bind(this));
|
||||
this.hideSubtitlesEl.on('click', this.toggle);
|
||||
this.subtitlesEl
|
||||
.on({
|
||||
mouseenter: this.onMouseEnter.bind(this),
|
||||
mouseleave: this.onMouseLeave.bind(this),
|
||||
mousemove: this.onMovement.bind(this),
|
||||
mousewheel: this.onMovement.bind(this),
|
||||
DOMMouseScroll: this.onMovement.bind(this)
|
||||
mouseenter: this.onMouseEnter,
|
||||
mouseleave: this.onMouseLeave,
|
||||
mousemove: this.onMovement,
|
||||
mousewheel: this.onMovement,
|
||||
DOMMouseScroll: this.onMovement
|
||||
})
|
||||
.on(events, 'li[data-index]', function (event) {
|
||||
switch (event.type) {
|
||||
case 'mouseover':
|
||||
case 'mouseout':
|
||||
self.captionMouseOverOut(event);
|
||||
break;
|
||||
case 'mousedown':
|
||||
self.captionMouseDown(event);
|
||||
break;
|
||||
case 'click':
|
||||
self.captionClick(event);
|
||||
break;
|
||||
case 'focusin':
|
||||
self.captionFocus(event);
|
||||
break;
|
||||
case 'focusout':
|
||||
self.captionBlur(event);
|
||||
break;
|
||||
case 'keydown':
|
||||
self.captionKeyDown(event);
|
||||
break;
|
||||
}
|
||||
});
|
||||
.on(events, 'li[data-index]', this.onCaptionHandler);
|
||||
|
||||
if (this.showLanguageMenu) {
|
||||
this.container.on({
|
||||
mouseenter: this.onContainerMouseEnter.bind(this),
|
||||
mouseleave: this.onContainerMouseLeave.bind(this)
|
||||
mouseenter: this.onContainerMouseEnter,
|
||||
mouseleave: this.onContainerMouseLeave
|
||||
});
|
||||
}
|
||||
|
||||
state.el
|
||||
.on({
|
||||
'caption:fetch': this.fetchCaption.bind(this),
|
||||
'caption:resize': this.onResize.bind(this),
|
||||
'caption:update': function (event, time) {
|
||||
self.updatePlayTime(time);
|
||||
},
|
||||
'ended': this.pause.bind(this),
|
||||
'fullscreen': this.onResize.bind(this),
|
||||
'pause': this.pause.bind(this),
|
||||
'play': this.play.bind(this)
|
||||
'caption:fetch': this.fetchCaption,
|
||||
'caption:resize': this.onResize,
|
||||
'caption:update': this.onCaptionUpdate,
|
||||
'ended': this.pause,
|
||||
'fullscreen': this.onResize,
|
||||
'pause': this.pause,
|
||||
'play': this.play,
|
||||
'destroy': this.destroy
|
||||
});
|
||||
|
||||
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
|
||||
@@ -130,6 +140,33 @@ function (Sjson, AsyncProcess) {
|
||||
}
|
||||
},
|
||||
|
||||
onCaptionUpdate: function (event, time) {
|
||||
this.updatePlayTime(time);
|
||||
},
|
||||
|
||||
onCaptionHandler: function (event) {
|
||||
switch (event.type) {
|
||||
case 'mouseover':
|
||||
case 'mouseout':
|
||||
this.captionMouseOverOut(event);
|
||||
break;
|
||||
case 'mousedown':
|
||||
this.captionMouseDown(event);
|
||||
break;
|
||||
case 'click':
|
||||
this.captionClick(event);
|
||||
break;
|
||||
case 'focusin':
|
||||
this.captionFocus(event);
|
||||
break;
|
||||
case 'focusout':
|
||||
this.captionBlur(event);
|
||||
break;
|
||||
case 'keydown':
|
||||
this.captionKeyDown(event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Opens language menu.
|
||||
@@ -138,8 +175,8 @@ function (Sjson, AsyncProcess) {
|
||||
*/
|
||||
onContainerMouseEnter: function (event) {
|
||||
event.preventDefault();
|
||||
this.state.videoPlayer.log('video_show_cc_menu', {});
|
||||
$(event.currentTarget).addClass('is-opened');
|
||||
this.state.el.trigger('language_menu:show');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -149,8 +186,8 @@ function (Sjson, AsyncProcess) {
|
||||
*/
|
||||
onContainerMouseLeave: function (event) {
|
||||
event.preventDefault();
|
||||
this.state.videoPlayer.log('video_hide_cc_menu', {});
|
||||
$(event.currentTarget).removeClass('is-opened');
|
||||
this.state.el.trigger('language_menu:hide');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -247,12 +284,11 @@ function (Sjson, AsyncProcess) {
|
||||
var self = this,
|
||||
state = this.state,
|
||||
language = state.getCurrentLanguage(),
|
||||
url = state.config.transcriptTranslationUrl.replace('__lang__', language),
|
||||
data, youtubeId;
|
||||
|
||||
if (this.loaded) {
|
||||
this.hideCaptions(false);
|
||||
} else {
|
||||
this.hideCaptions(state.hide_captions, false);
|
||||
}
|
||||
|
||||
if (this.fetchXHR && this.fetchXHR.abort) {
|
||||
@@ -266,16 +302,14 @@ function (Sjson, AsyncProcess) {
|
||||
return false;
|
||||
}
|
||||
|
||||
data = {
|
||||
videoId: youtubeId
|
||||
};
|
||||
data = {videoId: youtubeId};
|
||||
}
|
||||
|
||||
state.el.removeClass('is-captions-rendered');
|
||||
// Fetch the captions file. If no file was specified, or if an error
|
||||
// occurred, then we hide the captions panel, and the "CC" button
|
||||
this.fetchXHR = $.ajaxWithPrefix({
|
||||
url: state.config.transcriptTranslationUrl + '/' + language,
|
||||
url: url,
|
||||
notifyOnError: false,
|
||||
data: data,
|
||||
success: function (sjson) {
|
||||
@@ -300,7 +334,9 @@ function (Sjson, AsyncProcess) {
|
||||
} else {
|
||||
self.renderCaption(start, captions);
|
||||
}
|
||||
|
||||
self.hideCaptions(state.hide_captions, false);
|
||||
self.state.el.find('.video-wrapper').after(self.subtitlesEl);
|
||||
self.state.el.find('.secondary-controls').append(self.container);
|
||||
self.bindHandlers();
|
||||
}
|
||||
|
||||
@@ -336,7 +372,7 @@ function (Sjson, AsyncProcess) {
|
||||
var self = this,
|
||||
state = this.state;
|
||||
|
||||
return $.ajaxWithPrefix({
|
||||
this.availableTranslationsXHR = $.ajaxWithPrefix({
|
||||
url: state.config.transcriptAvailableTranslationsUrl,
|
||||
notifyOnError: false,
|
||||
success: function (response) {
|
||||
@@ -359,6 +395,8 @@ function (Sjson, AsyncProcess) {
|
||||
self.hideSubtitlesEl.hide();
|
||||
}
|
||||
});
|
||||
|
||||
return this.availableTranslationsXHR;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -417,11 +455,11 @@ function (Sjson, AsyncProcess) {
|
||||
|
||||
if (state.lang !== langCode) {
|
||||
state.lang = langCode;
|
||||
state.storage.setItem('language', langCode);
|
||||
el .addClass('is-active')
|
||||
.siblings('li')
|
||||
.removeClass('is-active');
|
||||
|
||||
state.el.trigger('language_menu:change', [langCode]);
|
||||
self.fetchCaption();
|
||||
}
|
||||
});
|
||||
@@ -658,7 +696,7 @@ function (Sjson, AsyncProcess) {
|
||||
*
|
||||
*/
|
||||
play: function () {
|
||||
var startAndCaptions, start, end;
|
||||
var captions, startAndCaptions, start;
|
||||
if (this.loaded) {
|
||||
if (!this.rendered) {
|
||||
startAndCaptions = this.getBoundedCaptions();
|
||||
@@ -689,10 +727,7 @@ function (Sjson, AsyncProcess) {
|
||||
*/
|
||||
updatePlayTime: function (time) {
|
||||
var state = this.state,
|
||||
startTime,
|
||||
endTime,
|
||||
params,
|
||||
newIndex;
|
||||
params, newIndex;
|
||||
|
||||
if (this.loaded) {
|
||||
if (state.isFlashMode()) {
|
||||
@@ -797,9 +832,9 @@ function (Sjson, AsyncProcess) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.state.el.hasClass('closed')) {
|
||||
this.hideCaptions(false);
|
||||
this.hideCaptions(false, true, true);
|
||||
} else {
|
||||
this.hideCaptions(true);
|
||||
this.hideCaptions(true, true, true);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -811,38 +846,35 @@ function (Sjson, AsyncProcess) {
|
||||
* @param {boolean} update_cookie Flag to update or not the cookie.
|
||||
*
|
||||
*/
|
||||
hideCaptions: function (hide_captions, update_cookie) {
|
||||
hideCaptions: function (hide_captions, update_cookie, trigger_event) {
|
||||
var hideSubtitlesEl = this.hideSubtitlesEl,
|
||||
state = this.state,
|
||||
type, text;
|
||||
state = this.state, text;
|
||||
|
||||
if (typeof update_cookie === 'undefined') {
|
||||
update_cookie = true;
|
||||
}
|
||||
|
||||
if (hide_captions) {
|
||||
type = 'hide_transcript';
|
||||
state.captionsHidden = true;
|
||||
state.el.addClass('closed');
|
||||
text = gettext('Turn on captions');
|
||||
if (trigger_event) {
|
||||
this.state.el.trigger('captions:hide');
|
||||
}
|
||||
} else {
|
||||
type = 'show_transcript';
|
||||
state.captionsHidden = false;
|
||||
state.el.removeClass('closed');
|
||||
this.scrollCaption();
|
||||
text = gettext('Turn off captions');
|
||||
if (trigger_event) {
|
||||
this.state.el.trigger('captions:show');
|
||||
}
|
||||
}
|
||||
|
||||
hideSubtitlesEl
|
||||
.attr('title', text)
|
||||
.text(gettext(text));
|
||||
|
||||
if (state.videoPlayer) {
|
||||
state.videoPlayer.log(type, {
|
||||
currentTime: state.videoPlayer.currentTime
|
||||
});
|
||||
}
|
||||
|
||||
if (state.resizer) {
|
||||
if (state.isFullScreen) {
|
||||
state.resizer.setMode('both');
|
||||
@@ -868,9 +900,8 @@ function (Sjson, AsyncProcess) {
|
||||
*/
|
||||
captionHeight: function () {
|
||||
var state = this.state;
|
||||
|
||||
if (state.isFullScreen) {
|
||||
return state.container.height() - state.videoControl.height;
|
||||
return state.container.height() - state.videoFullScreen.height;
|
||||
} else {
|
||||
return state.container.height();
|
||||
}
|
||||
@@ -889,8 +920,8 @@ function (Sjson, AsyncProcess) {
|
||||
) {
|
||||
// In case of html5 autoshowing subtitles, we adjust height of
|
||||
// subs, by height of scrollbar.
|
||||
height = state.videoControl.el.height() +
|
||||
0.5 * state.videoControl.sliderEl.height();
|
||||
height = state.el.find('.video-controls').height() +
|
||||
0.5 * state.el.find('.slider').height();
|
||||
// Height of videoControl does not contain height of slider.
|
||||
// css is set to absolute, to avoid yanking when slider
|
||||
// autochanges its height.
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
// VideoCommands module.
|
||||
define('video/10_commands.js', [], function() {
|
||||
var VideoCommands, Command, playCommand, pauseCommand, togglePlaybackCommand,
|
||||
muteCommand, unmuteCommand, toggleMuteCommand, toggleFullScreenCommand,
|
||||
setSpeedCommand;
|
||||
|
||||
toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand, skipCommand;
|
||||
/**
|
||||
* Video commands module.
|
||||
* @exports video/10_commands.js
|
||||
@@ -19,6 +16,7 @@ define('video/10_commands.js', [], function() {
|
||||
return new VideoCommands(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'destroy');
|
||||
this.state = state;
|
||||
this.state.videoCommands = this;
|
||||
this.i18n = i18n;
|
||||
@@ -29,9 +27,15 @@ define('video/10_commands.js', [], function() {
|
||||
};
|
||||
|
||||
VideoCommands.prototype = {
|
||||
destroy: function () {
|
||||
this.state.el.off('destroy', this.destroy);
|
||||
delete this.state.videoCommands;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
this.commands = this.getCommands();
|
||||
this.state.el.on('destroy', this.destroy);
|
||||
},
|
||||
|
||||
execute: function (command) {
|
||||
@@ -48,7 +52,8 @@ define('video/10_commands.js', [], function() {
|
||||
var commands = {},
|
||||
commandsList = [
|
||||
playCommand, pauseCommand, togglePlaybackCommand,
|
||||
toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand
|
||||
toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand,
|
||||
skipCommand
|
||||
];
|
||||
|
||||
_.each(commandsList, function(command) {
|
||||
@@ -73,7 +78,7 @@ define('video/10_commands.js', [], function() {
|
||||
});
|
||||
|
||||
togglePlaybackCommand = new Command('togglePlayback', function (state) {
|
||||
if (state.videoControl.isPlaying) {
|
||||
if (state.videoPlayer.isPlaying()) {
|
||||
pauseCommand.execute(state);
|
||||
} else {
|
||||
playCommand.execute(state);
|
||||
@@ -85,13 +90,21 @@ define('video/10_commands.js', [], function() {
|
||||
});
|
||||
|
||||
toggleFullScreenCommand = new Command('toggleFullScreen', function (state) {
|
||||
state.videoControl.toggleFullScreen();
|
||||
state.videoFullScreen.toggle();
|
||||
});
|
||||
|
||||
setSpeedCommand = new Command('speed', function (state, speed) {
|
||||
state.videoSpeedControl.setSpeed(state.speedToString(speed));
|
||||
});
|
||||
|
||||
skipCommand = new Command('skip', function (state, doNotShowAgain) {
|
||||
if (doNotShowAgain) {
|
||||
state.videoBumper.skipAndDoNotShowAgain();
|
||||
} else {
|
||||
state.videoBumper.skip();
|
||||
}
|
||||
});
|
||||
|
||||
return VideoCommands;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
(function (require, $) {
|
||||
'use strict';
|
||||
|
||||
// In the case when the Video constructor will be called before RequireJS finishes loading all of the Video
|
||||
// dependencies, we will have a mock function that will collect all the elements that must be initialized as
|
||||
// Video elements.
|
||||
@@ -35,74 +34,122 @@
|
||||
// Main module.
|
||||
require(
|
||||
[
|
||||
'video/00_video_storage.js',
|
||||
'video/01_initialize.js',
|
||||
'video/025_focus_grabber.js',
|
||||
'video/035_video_accessible_menu.js',
|
||||
'video/04_video_control.js',
|
||||
'video/04_video_full_screen.js',
|
||||
'video/05_video_quality_control.js',
|
||||
'video/06_video_progress_slider.js',
|
||||
'video/07_video_volume_control.js',
|
||||
'video/08_video_speed_control.js',
|
||||
'video/09_video_caption.js',
|
||||
'video/09_play_placeholder.js',
|
||||
'video/09_play_pause_control.js',
|
||||
'video/09_play_skip_control.js',
|
||||
'video/09_skip_control.js',
|
||||
'video/09_bumper.js',
|
||||
'video/09_save_state_plugin.js',
|
||||
'video/09_events_plugin.js',
|
||||
'video/09_events_bumper_plugin.js',
|
||||
'video/09_poster.js',
|
||||
'video/10_commands.js',
|
||||
'video/095_video_context_menu.js'
|
||||
],
|
||||
function (
|
||||
initialize,
|
||||
FocusGrabber,
|
||||
VideoAccessibleMenu,
|
||||
VideoControl,
|
||||
VideoQualityControl,
|
||||
VideoProgressSlider,
|
||||
VideoVolumeControl,
|
||||
VideoSpeedControl,
|
||||
VideoCaption,
|
||||
VideoCommands,
|
||||
VideoStorage, initialize, FocusGrabber, VideoAccessibleMenu, VideoControl, VideoFullScreen,
|
||||
VideoQualityControl, VideoProgressSlider, VideoVolumeControl, VideoSpeedControl, VideoCaption,
|
||||
VideoPlayPlaceholder, VideoPlayPauseControl, VideoPlaySkipControl, VideoSkipControl, VideoBumper,
|
||||
VideoSaveStatePlugin, VideoEventsPlugin, VideoEventsBumperPlugin, VideoPoster, VideoCommands,
|
||||
VideoContextMenu
|
||||
) {
|
||||
var youtubeXhr = null,
|
||||
oldVideo = window.Video;
|
||||
|
||||
window.Video = function (element) {
|
||||
var previousState = window.Video.previousState,
|
||||
state;
|
||||
var el = $(element).find('.video'),
|
||||
id = el.attr('id').replace(/video_/, ''),
|
||||
storage = VideoStorage('VideoState', id),
|
||||
bumperMetadata = el.data('bumper-metadata'),
|
||||
mainVideoModules = [FocusGrabber, VideoControl, VideoPlayPlaceholder,
|
||||
VideoPlayPauseControl, VideoProgressSlider, VideoSpeedControl, VideoVolumeControl,
|
||||
VideoQualityControl, VideoFullScreen, VideoCaption, VideoCommands, VideoContextMenu,
|
||||
VideoSaveStatePlugin, VideoEventsPlugin],
|
||||
bumperVideoModules = [VideoControl, VideoPlaySkipControl, VideoSkipControl,
|
||||
VideoVolumeControl, VideoCaption, VideoCommands, VideoSaveStatePlugin, VideoEventsBumperPlugin],
|
||||
state = {
|
||||
el: el,
|
||||
id: id,
|
||||
metadata: el.data('metadata'),
|
||||
storage: storage,
|
||||
options: {},
|
||||
youtubeXhr: youtubeXhr,
|
||||
modules: mainVideoModules
|
||||
};
|
||||
|
||||
// Check for existance of previous state, uninitialize it if necessary, and create a new state. Store
|
||||
// new state for future invocation of this module consturctor function.
|
||||
if (previousState && previousState.videoPlayer) {
|
||||
previousState.saveState(true);
|
||||
$(window).off('unload', previousState.saveState);
|
||||
var getBumperState = function (metadata) {
|
||||
var bumperState = $.extend(true, {
|
||||
el: el,
|
||||
id: id,
|
||||
storage: storage,
|
||||
options: {},
|
||||
youtubeXhr: youtubeXhr
|
||||
}, {metadata: metadata});
|
||||
|
||||
bumperState.modules = bumperVideoModules;
|
||||
bumperState.options = {
|
||||
SaveStatePlugin: {events: ['language_menu:change']}
|
||||
};
|
||||
return bumperState;
|
||||
};
|
||||
|
||||
var player = function (state) {
|
||||
return function () {
|
||||
_.extend(state.metadata, {autoplay: true, focusFirstControl: true});
|
||||
initialize(state, element);
|
||||
};
|
||||
};
|
||||
|
||||
new VideoAccessibleMenu(el, {
|
||||
storage: storage,
|
||||
saveStateUrl: state.metadata.saveStateUrl
|
||||
});
|
||||
|
||||
if (bumperMetadata) {
|
||||
new VideoPoster(el, {
|
||||
poster: el.data('poster'),
|
||||
onClick: _.once(function () {
|
||||
var mainVideoPlayer = player(state), bumper, bumperState;
|
||||
if (storage.getItem('isBumperShown')) {
|
||||
mainVideoPlayer();
|
||||
} else {
|
||||
bumperState = getBumperState(bumperMetadata);
|
||||
bumper = new VideoBumper(player(bumperState), bumperState);
|
||||
state.bumperState = bumperState;
|
||||
bumper.getPromise().done(function () {
|
||||
delete state.bumperState;
|
||||
mainVideoPlayer();
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
} else {
|
||||
initialize(state, element);
|
||||
}
|
||||
|
||||
state = {};
|
||||
// Because this constructor can be called multiple times on a single page (when the user switches
|
||||
// verticals, the page doesn't reload, but the content changes), we must will check each time if there
|
||||
// is a previous copy of 'state' object. If there is, we will make sure that copy exists cleanly. We
|
||||
// have to do this because when verticals switch, the code does not handle any Xmodule JS code that is
|
||||
// running - it simply removes DOM elements from the page. Any functions that were running during this,
|
||||
// and that will run afterwards (expecting the DOM elements to be present) must be stopped by hand.
|
||||
window.Video.previousState = state;
|
||||
|
||||
state.modules = [
|
||||
FocusGrabber,
|
||||
VideoAccessibleMenu,
|
||||
VideoControl,
|
||||
VideoQualityControl,
|
||||
VideoProgressSlider,
|
||||
VideoVolumeControl,
|
||||
VideoSpeedControl,
|
||||
VideoCaption,
|
||||
VideoCommands,
|
||||
VideoContextMenu
|
||||
];
|
||||
|
||||
state.youtubeXhr = youtubeXhr;
|
||||
initialize(state, element);
|
||||
if (!youtubeXhr) {
|
||||
youtubeXhr = state.youtubeXhr;
|
||||
}
|
||||
|
||||
$(element).find('.video').data('video-player-state', state);
|
||||
el.data('video-player-state', state);
|
||||
var onSequenceChange = function onSequenceChange () {
|
||||
if (state && state.videoPlayer) {
|
||||
state.videoPlayer.destroy();
|
||||
}
|
||||
$('.sequence').off('sequence:change', onSequenceChange);
|
||||
};
|
||||
$('.sequence').on('sequence:change', onSequenceChange);
|
||||
|
||||
// Because the 'state' object is only available inside this closure, we will also make it available to
|
||||
// the caller by returning it. This is necessary so that we can test Video with Jasmine.
|
||||
|
||||
@@ -153,6 +153,18 @@ class InheritanceMixin(XBlockMixin):
|
||||
default=True,
|
||||
scope=Scope.settings
|
||||
)
|
||||
video_bumper = Dict(
|
||||
display_name=_("Video Pre-Roll"),
|
||||
help=_(
|
||||
"""Identify a video, 5-10 seconds in length, to play before course videos. Enter the video ID from"""
|
||||
""" the Video Uploads page and one or more transcript files in the following format:"""
|
||||
""" {"video_id": "ID", "transcripts": {"language": "/static/filename.srt"}}."""
|
||||
""" For example, an entry for a video with two transcripts looks like this:"""
|
||||
""" {"video_id": "77cef264-d6f5-4cf2-ad9d-0178ab8c77be","""
|
||||
""" "transcripts": {"en": "/static/DemoX-D01_1.srt", "uk": "/static/DemoX-D01_1_uk.srt"}}"""
|
||||
),
|
||||
scope=Scope.settings
|
||||
)
|
||||
|
||||
reset_key = "DEFAULT_SHOW_RESET_BUTTON"
|
||||
default_reset_button = getattr(settings, reset_key) if hasattr(settings, reset_key) else False
|
||||
|
||||
@@ -8,3 +8,4 @@ Container for video module and it's utils.
|
||||
from .transcripts_utils import *
|
||||
from .video_utils import *
|
||||
from .video_module import *
|
||||
from .bumper_utils import *
|
||||
|
||||
142
common/lib/xmodule/xmodule/video_module/bumper_utils.py
Normal file
142
common/lib/xmodule/xmodule/video_module/bumper_utils.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
Utils for video bumper
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import pytz
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from django.conf import settings
|
||||
|
||||
from .video_utils import set_query_parameter
|
||||
|
||||
try:
|
||||
import edxval.api as edxval_api
|
||||
except ImportError:
|
||||
edxval_api = None
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_bumper_settings(video):
|
||||
"""
|
||||
Get bumper settings from video instance.
|
||||
"""
|
||||
bumper_settings = copy.deepcopy(getattr(video, 'video_bumper', {}))
|
||||
|
||||
# clean up /static/ prefix from bumper transcripts
|
||||
for lang, transcript_url in bumper_settings.get('transcripts', {}).items():
|
||||
bumper_settings['transcripts'][lang] = transcript_url.replace("/static/", "")
|
||||
|
||||
return bumper_settings
|
||||
|
||||
|
||||
def is_bumper_enabled(video):
|
||||
"""
|
||||
Check if bumper enabled.
|
||||
|
||||
- Feature flag ENABLE_VIDEO_BUMPER should be set to True
|
||||
- Do not show again button should not be clicked by user.
|
||||
- Current time minus periodicity must be greater that last time viewed
|
||||
- edxval_api should be presented
|
||||
|
||||
Returns:
|
||||
bool.
|
||||
"""
|
||||
bumper_last_view_date = getattr(video, 'bumper_last_view_date', None)
|
||||
utc_now = datetime.utcnow().replace(tzinfo=pytz.utc)
|
||||
periodicity = settings.FEATURES.get('SHOW_BUMPER_PERIODICITY', 0)
|
||||
has_viewed = any([
|
||||
getattr(video, 'bumper_do_not_show_again'),
|
||||
(bumper_last_view_date and bumper_last_view_date + timedelta(seconds=periodicity) > utc_now)
|
||||
])
|
||||
is_studio = getattr(video.system, "is_author_mode", False)
|
||||
return bool(
|
||||
not is_studio and
|
||||
settings.FEATURES.get('ENABLE_VIDEO_BUMPER') and
|
||||
get_bumper_settings(video) and
|
||||
edxval_api and
|
||||
not has_viewed
|
||||
)
|
||||
|
||||
|
||||
def bumperize(video):
|
||||
"""
|
||||
Populate video with bumper settings, if they are presented.
|
||||
"""
|
||||
video.bumper = {
|
||||
'enabled': False,
|
||||
'edx_video_id': "",
|
||||
'transcripts': {},
|
||||
'metadata': None,
|
||||
}
|
||||
|
||||
if not is_bumper_enabled(video):
|
||||
return
|
||||
|
||||
bumper_settings = get_bumper_settings(video)
|
||||
|
||||
try:
|
||||
video.bumper['edx_video_id'] = bumper_settings['video_id']
|
||||
video.bumper['transcripts'] = bumper_settings['transcripts']
|
||||
except (TypeError, KeyError):
|
||||
log.warning(
|
||||
"Could not retrieve video bumper information from course settings"
|
||||
)
|
||||
return
|
||||
|
||||
sources = get_bumper_sources(video)
|
||||
if not sources:
|
||||
return
|
||||
|
||||
video.bumper.update({
|
||||
'metadata': bumper_metadata(video, sources),
|
||||
'enabled': True, # Video poster needs this.
|
||||
})
|
||||
|
||||
|
||||
def get_bumper_sources(video):
|
||||
"""
|
||||
Get bumper sources from edxval.
|
||||
|
||||
Returns list of sources.
|
||||
"""
|
||||
try:
|
||||
val_profiles = ["desktop_webm", "desktop_mp4"]
|
||||
val_video_urls = edxval_api.get_urls_for_profiles(video.bumper['edx_video_id'], val_profiles)
|
||||
bumper_sources = filter(None, [val_video_urls[p] for p in val_profiles])
|
||||
except edxval_api.ValInternalError:
|
||||
# if no bumper sources, nothing will be showed
|
||||
log.warning(
|
||||
"Could not retrieve information from VAL for Bumper edx Video ID: %s.", video.bumper['edx_video_id']
|
||||
)
|
||||
return []
|
||||
|
||||
return bumper_sources
|
||||
|
||||
|
||||
def bumper_metadata(video, sources):
|
||||
"""
|
||||
Generate bumper metadata.
|
||||
"""
|
||||
transcripts = video.get_transcripts_info(is_bumper=True)
|
||||
unused_track_url, bumper_transcript_language, bumper_languages = video.get_transcripts_for_student(transcripts)
|
||||
|
||||
metadata = OrderedDict({
|
||||
'saveStateUrl': video.system.ajax_url + '/save_user_state',
|
||||
'showCaptions': json.dumps(video.show_captions),
|
||||
'sources': sources,
|
||||
'streams': '',
|
||||
'transcriptLanguage': bumper_transcript_language,
|
||||
'transcriptLanguages': bumper_languages,
|
||||
'transcriptTranslationUrl': set_query_parameter(
|
||||
video.runtime.handler_url(video, 'transcript', 'translation/__lang__').rstrip('/?'), 'is_bumper', 1
|
||||
),
|
||||
'transcriptAvailableTranslationsUrl': set_query_parameter(
|
||||
video.runtime.handler_url(video, 'transcript', 'available_translations').rstrip('/?'), 'is_bumper', 1
|
||||
),
|
||||
})
|
||||
|
||||
return metadata
|
||||
@@ -15,6 +15,8 @@ from xmodule.exceptions import NotFoundError
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import contentstore
|
||||
|
||||
from .bumper_utils import get_bumper_settings
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -408,20 +410,23 @@ def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang):
|
||||
)
|
||||
|
||||
|
||||
def get_or_create_sjson(item):
|
||||
def get_or_create_sjson(item, transcripts):
|
||||
"""
|
||||
Get sjson if already exists, otherwise generate it.
|
||||
|
||||
Generate sjson with subs_id name, from user uploaded srt.
|
||||
Subs_id is extracted from srt filename, which was set by user.
|
||||
|
||||
Args:
|
||||
transcipts (dict): dictionary of (language: file) pairs.
|
||||
|
||||
Raises:
|
||||
TranscriptException: when srt subtitles do not exist,
|
||||
and exceptions from generate_subs_from_source.
|
||||
|
||||
`item` is module object.
|
||||
"""
|
||||
user_filename = item.transcripts[item.transcript_language]
|
||||
user_filename = transcripts[item.transcript_language]
|
||||
user_subs_id = os.path.splitext(user_filename)[0]
|
||||
source_subs_id, result_subs_dict = user_subs_id, {1.0: user_subs_id}
|
||||
try:
|
||||
@@ -517,7 +522,7 @@ class VideoTranscriptsMixin(object):
|
||||
This is necessary for both VideoModule and VideoDescriptor.
|
||||
"""
|
||||
|
||||
def available_translations(self, verify_assets=True):
|
||||
def available_translations(self, transcripts, verify_assets=True):
|
||||
"""Return a list of language codes for which we have transcripts.
|
||||
|
||||
Args:
|
||||
@@ -528,39 +533,51 @@ class VideoTranscriptsMixin(object):
|
||||
when trying to make a listing of videos and their languages.
|
||||
|
||||
Defaults to True.
|
||||
|
||||
transcripts (dict): A dict with all transcripts and a sub.
|
||||
|
||||
Defaults to False
|
||||
"""
|
||||
translations = []
|
||||
sub, other_lang = transcripts["sub"], transcripts["transcripts"]
|
||||
|
||||
# If we're not verifying the assets, we just trust our field values
|
||||
if not verify_assets:
|
||||
translations = list(self.transcripts)
|
||||
if not translations or self.sub:
|
||||
translations = list(other_lang)
|
||||
if not translations or sub:
|
||||
translations += ['en']
|
||||
return set(translations)
|
||||
|
||||
# If we've gotten this far, we're going to verify that the transcripts
|
||||
# being referenced are actually in the contentstore.
|
||||
if self.sub: # check if sjson exists for 'en'.
|
||||
if sub: # check if sjson exists for 'en'.
|
||||
try:
|
||||
Transcript.asset(self.location, self.sub, 'en')
|
||||
Transcript.asset(self.location, sub, 'en')
|
||||
except NotFoundError:
|
||||
pass
|
||||
try:
|
||||
Transcript.asset(self.location, None, None, sub)
|
||||
except NotFoundError:
|
||||
pass
|
||||
else:
|
||||
translations = ['en']
|
||||
else:
|
||||
translations = ['en']
|
||||
|
||||
for lang in self.transcripts:
|
||||
for lang in other_lang:
|
||||
try:
|
||||
Transcript.asset(self.location, None, None, self.transcripts[lang])
|
||||
Transcript.asset(self.location, None, None, other_lang[lang])
|
||||
except NotFoundError:
|
||||
continue
|
||||
translations.append(lang)
|
||||
|
||||
return translations
|
||||
|
||||
def get_transcript(self, transcript_format='srt', lang=None):
|
||||
def get_transcript(self, transcripts, transcript_format='srt', lang=None):
|
||||
"""
|
||||
Returns transcript, filename and MIME type.
|
||||
|
||||
transcripts (dict): A dict with all transcripts and a sub.
|
||||
|
||||
Raises:
|
||||
- NotFoundError if cannot find transcript file in storage.
|
||||
- ValueError if transcript file is empty or incorrect JSON.
|
||||
@@ -572,11 +589,12 @@ class VideoTranscriptsMixin(object):
|
||||
If language is not 'en', give back transcript in proper language and format.
|
||||
"""
|
||||
if not lang:
|
||||
lang = self.transcript_language
|
||||
lang = self.get_default_transcript_language(transcripts)
|
||||
|
||||
sub, other_lang = transcripts["sub"], transcripts["transcripts"]
|
||||
if lang == 'en':
|
||||
if self.sub: # HTML5 case and (Youtube case for new style videos)
|
||||
transcript_name = self.sub
|
||||
if sub: # HTML5 case and (Youtube case for new style videos)
|
||||
transcript_name = sub
|
||||
elif self.youtube_id_1_0: # old courses
|
||||
transcript_name = self.youtube_id_1_0
|
||||
else:
|
||||
@@ -587,8 +605,8 @@ class VideoTranscriptsMixin(object):
|
||||
filename = u'{}.{}'.format(transcript_name, transcript_format)
|
||||
content = Transcript.convert(data, 'sjson', transcript_format)
|
||||
else:
|
||||
data = Transcript.asset(self.location, None, None, self.transcripts[lang]).data
|
||||
filename = u'{}.{}'.format(os.path.splitext(self.transcripts[lang])[0], transcript_format)
|
||||
data = Transcript.asset(self.location, None, None, other_lang[lang]).data
|
||||
filename = u'{}.{}'.format(os.path.splitext(other_lang[lang])[0], transcript_format)
|
||||
content = Transcript.convert(data, 'srt', transcript_format)
|
||||
|
||||
if not content:
|
||||
@@ -597,16 +615,36 @@ class VideoTranscriptsMixin(object):
|
||||
|
||||
return content, filename, Transcript.mime_types[transcript_format]
|
||||
|
||||
def get_default_transcript_language(self):
|
||||
def get_default_transcript_language(self, transcripts):
|
||||
"""
|
||||
Returns the default transcript language for this video module.
|
||||
|
||||
Args:
|
||||
transcripts (dict): A dict with all transcripts and a sub.
|
||||
"""
|
||||
if self.transcript_language in self.transcripts:
|
||||
sub, other_lang = transcripts["sub"], transcripts["transcripts"]
|
||||
if self.transcript_language in other_lang:
|
||||
transcript_language = self.transcript_language
|
||||
elif self.sub:
|
||||
elif sub:
|
||||
transcript_language = u'en'
|
||||
elif len(self.transcripts) > 0:
|
||||
transcript_language = sorted(self.transcripts)[0]
|
||||
elif len(other_lang) > 0:
|
||||
transcript_language = sorted(other_lang)[0]
|
||||
else:
|
||||
transcript_language = u'en'
|
||||
return transcript_language
|
||||
|
||||
def get_transcripts_info(self, is_bumper=False):
|
||||
"""
|
||||
Returns a transcript dictionary for the video.
|
||||
"""
|
||||
if is_bumper:
|
||||
transcripts = copy.deepcopy(get_bumper_settings(self).get('transcripts', {}))
|
||||
return {
|
||||
"sub": transcripts.pop("en", ""),
|
||||
"transcripts": transcripts,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"sub": self.sub,
|
||||
"transcripts": self.transcripts,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ StudioViewHandlers are handlers for video descriptor instance.
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from webob import Response
|
||||
|
||||
from xblock.core import XBlock
|
||||
@@ -44,7 +45,8 @@ class VideoStudentViewHandlers(object):
|
||||
"""
|
||||
accepted_keys = [
|
||||
'speed', 'saved_video_position', 'transcript_language',
|
||||
'transcript_download_format', 'youtube_is_available'
|
||||
'transcript_download_format', 'youtube_is_available',
|
||||
'bumper_last_view_date', 'bumper_do_not_show_again'
|
||||
]
|
||||
|
||||
conversions = {
|
||||
@@ -61,6 +63,9 @@ class VideoStudentViewHandlers(object):
|
||||
else:
|
||||
value = data[key]
|
||||
|
||||
if key == 'bumper_last_view_date':
|
||||
value = datetime.utcnow()
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
if key == 'speed':
|
||||
@@ -73,16 +78,17 @@ class VideoStudentViewHandlers(object):
|
||||
|
||||
raise NotFoundError('Unexpected dispatch type')
|
||||
|
||||
def translation(self, youtube_id):
|
||||
def translation(self, youtube_id, transcripts):
|
||||
"""
|
||||
This is called to get transcript file for specific language.
|
||||
|
||||
youtube_id: str: must be one of youtube_ids or None if HTML video
|
||||
transcripts (dict): A dict with all transcripts and a sub.
|
||||
|
||||
Logic flow:
|
||||
|
||||
If youtube_id doesn't exist, we have a video in HTML5 mode. Otherwise,
|
||||
video video in Youtube or Flash modes.
|
||||
video in Youtube or Flash modes.
|
||||
|
||||
if youtube:
|
||||
If english -> give back youtube_id subtitles:
|
||||
@@ -106,6 +112,7 @@ class VideoStudentViewHandlers(object):
|
||||
NotFoundError if for 'en' subtitles no asset is uploaded.
|
||||
NotFoundError if youtube_id does not exist / invalid youtube_id
|
||||
"""
|
||||
sub, other_lang = transcripts["sub"], transcripts["transcripts"]
|
||||
if youtube_id:
|
||||
# Youtube case:
|
||||
if self.transcript_language == 'en':
|
||||
@@ -122,7 +129,7 @@ class VideoStudentViewHandlers(object):
|
||||
log.info("Can't find content in storage for %s transcript: generating.", youtube_id)
|
||||
generate_sjson_for_all_speeds(
|
||||
self,
|
||||
self.transcripts[self.transcript_language],
|
||||
other_lang[self.transcript_language],
|
||||
{speed: youtube_id for youtube_id, speed in youtube_ids.iteritems()},
|
||||
self.transcript_language
|
||||
)
|
||||
@@ -132,11 +139,18 @@ class VideoStudentViewHandlers(object):
|
||||
else:
|
||||
# HTML5 case
|
||||
if self.transcript_language == 'en':
|
||||
return Transcript.asset(self.location, self.sub).data
|
||||
else:
|
||||
return get_or_create_sjson(self)
|
||||
if '.srt' not in sub: # not bumper case
|
||||
return Transcript.asset(self.location, sub).data
|
||||
try:
|
||||
return get_or_create_sjson(self, {'en': sub})
|
||||
except TranscriptException:
|
||||
pass # to raise NotFoundError and try to get data in get_static_transcript
|
||||
elif other_lang:
|
||||
return get_or_create_sjson(self, other_lang)
|
||||
|
||||
def get_static_transcript(self, request):
|
||||
raise NotFoundError
|
||||
|
||||
def get_static_transcript(self, request, transcripts):
|
||||
"""
|
||||
Courses that are imported with the --nostatic flag do not show
|
||||
transcripts/captions properly even if those captions are stored inside
|
||||
@@ -144,6 +158,8 @@ class VideoStudentViewHandlers(object):
|
||||
the static asset path of the course if the transcript can't be found
|
||||
inside the contentstore and the course has the static_asset_path field
|
||||
set.
|
||||
|
||||
transcripts (dict): A dict with all transcripts and a sub.
|
||||
"""
|
||||
response = Response(status=404)
|
||||
# Only do redirect for English
|
||||
@@ -154,7 +170,7 @@ class VideoStudentViewHandlers(object):
|
||||
if video_id:
|
||||
transcript_name = video_id
|
||||
else:
|
||||
transcript_name = self.sub
|
||||
transcript_name = transcripts["sub"]
|
||||
|
||||
if transcript_name:
|
||||
# Get the asset path for course
|
||||
@@ -181,7 +197,9 @@ class VideoStudentViewHandlers(object):
|
||||
"""
|
||||
Entry point for transcript handlers for student_view.
|
||||
|
||||
Request GET may contain `videoId` for `translation` dispatch.
|
||||
Request GET contains:
|
||||
(optional) `videoId` for `translation` dispatch.
|
||||
`is_bumper=1` flag for bumper case.
|
||||
|
||||
Dispatches, (HTTP GET):
|
||||
/translation/[language_id]
|
||||
@@ -197,15 +215,16 @@ class VideoStudentViewHandlers(object):
|
||||
Returns list of languages, for which transcript files exist.
|
||||
For 'en' check if SJSON exists. For non-`en` check if SRT file exists.
|
||||
"""
|
||||
is_bumper = request.GET.get('is_bumper', False)
|
||||
transcripts = self.get_transcripts_info(is_bumper)
|
||||
if dispatch.startswith('translation'):
|
||||
|
||||
language = dispatch.replace('translation', '').strip('/')
|
||||
|
||||
if not language:
|
||||
log.info("Invalid /translation request: no language.")
|
||||
return Response(status=400)
|
||||
|
||||
if language not in ['en'] + self.transcripts.keys():
|
||||
if language not in ['en'] + transcripts["transcripts"].keys():
|
||||
log.info("Video: transcript facilities are not available for given language.")
|
||||
return Response(status=404)
|
||||
|
||||
@@ -213,12 +232,12 @@ class VideoStudentViewHandlers(object):
|
||||
self.transcript_language = language
|
||||
|
||||
try:
|
||||
transcript = self.translation(request.GET.get('videoId', None))
|
||||
transcript = self.translation(request.GET.get('videoId', None), transcripts)
|
||||
except (TypeError, NotFoundError) as ex:
|
||||
log.info(ex.message)
|
||||
# Try to return static URL redirection as last resort
|
||||
# if no translation is required
|
||||
return self.get_static_transcript(request)
|
||||
return self.get_static_transcript(request, transcripts)
|
||||
except (
|
||||
TranscriptException,
|
||||
UnicodeDecodeError,
|
||||
@@ -232,7 +251,9 @@ class VideoStudentViewHandlers(object):
|
||||
|
||||
elif dispatch == 'download':
|
||||
try:
|
||||
transcript_content, transcript_filename, transcript_mime_type = self.get_transcript(self.transcript_download_format)
|
||||
transcript_content, transcript_filename, transcript_mime_type = self.get_transcript(
|
||||
transcripts, transcript_format=self.transcript_download_format
|
||||
)
|
||||
except (NotFoundError, ValueError, KeyError, UnicodeDecodeError):
|
||||
log.debug("Video@download exception")
|
||||
return Response(status=404)
|
||||
@@ -246,8 +267,9 @@ class VideoStudentViewHandlers(object):
|
||||
)
|
||||
response.content_type = transcript_mime_type
|
||||
|
||||
elif dispatch == 'available_translations':
|
||||
available_translations = self.available_translations()
|
||||
elif dispatch.startswith('available_translations'):
|
||||
|
||||
available_translations = self.available_translations(transcripts)
|
||||
if available_translations:
|
||||
response = Response(json.dumps(available_translations))
|
||||
response.content_type = 'application/json'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# pylint: disable=abstract-method
|
||||
"""Video is ungraded Xmodule for support video content.
|
||||
@@ -37,7 +38,8 @@ from xmodule.xml_module import is_pointer_tag, name_to_pathname, deserialize_fie
|
||||
from xmodule.exceptions import NotFoundError
|
||||
|
||||
from .transcripts_utils import VideoTranscriptsMixin
|
||||
from .video_utils import create_youtube_string, get_video_from_cdn
|
||||
from .video_utils import create_youtube_string, get_video_from_cdn, get_poster
|
||||
from .bumper_utils import bumperize
|
||||
from .video_xfields import VideoFields
|
||||
from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers
|
||||
|
||||
@@ -117,11 +119,21 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
|
||||
resource_string(module, 'js/src/video/03_video_player.js'),
|
||||
resource_string(module, 'js/src/video/035_video_accessible_menu.js'),
|
||||
resource_string(module, 'js/src/video/04_video_control.js'),
|
||||
resource_string(module, 'js/src/video/04_video_full_screen.js'),
|
||||
resource_string(module, 'js/src/video/05_video_quality_control.js'),
|
||||
resource_string(module, 'js/src/video/06_video_progress_slider.js'),
|
||||
resource_string(module, 'js/src/video/07_video_volume_control.js'),
|
||||
resource_string(module, 'js/src/video/08_video_speed_control.js'),
|
||||
resource_string(module, 'js/src/video/09_video_caption.js'),
|
||||
resource_string(module, 'js/src/video/09_play_placeholder.js'),
|
||||
resource_string(module, 'js/src/video/09_play_pause_control.js'),
|
||||
resource_string(module, 'js/src/video/09_play_skip_control.js'),
|
||||
resource_string(module, 'js/src/video/09_skip_control.js'),
|
||||
resource_string(module, 'js/src/video/09_bumper.js'),
|
||||
resource_string(module, 'js/src/video/09_save_state_plugin.js'),
|
||||
resource_string(module, 'js/src/video/09_events_plugin.js'),
|
||||
resource_string(module, 'js/src/video/09_events_bumper_plugin.js'),
|
||||
resource_string(module, 'js/src/video/09_poster.js'),
|
||||
resource_string(module, 'js/src/video/095_video_context_menu.js'),
|
||||
resource_string(module, 'js/src/video/10_commands.js'),
|
||||
resource_string(module, 'js/src/video/10_main.js')
|
||||
@@ -133,9 +145,13 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
|
||||
]}
|
||||
js_module_name = "Video"
|
||||
|
||||
def get_transcripts_for_student(self):
|
||||
def get_transcripts_for_student(self, transcripts):
|
||||
"""Return transcript information necessary for rendering the XModule student view.
|
||||
This is more or less a direct extraction from `get_html`.
|
||||
|
||||
Args:
|
||||
transcripts (dict): A dict with all transcripts and a sub.
|
||||
|
||||
Returns:
|
||||
Tuple of (track_url, transcript_language, sorted_languages)
|
||||
track_url -> subtitle download url
|
||||
@@ -143,31 +159,27 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
|
||||
sorted_languages -> dictionary of available transcript languages
|
||||
"""
|
||||
track_url = None
|
||||
sub, other_lang = transcripts["sub"], transcripts["transcripts"]
|
||||
if self.download_track:
|
||||
if self.track:
|
||||
track_url = self.track
|
||||
elif self.sub or self.transcripts:
|
||||
elif sub or other_lang:
|
||||
track_url = self.runtime.handler_url(self, 'transcript', 'download').rstrip('/?')
|
||||
|
||||
if not self.transcripts:
|
||||
transcript_language = u'en'
|
||||
languages = {'en': 'English'}
|
||||
else:
|
||||
transcript_language = self.get_default_transcript_language()
|
||||
transcript_language = self.get_default_transcript_language(transcripts)
|
||||
|
||||
native_languages = {lang: label for lang, label in settings.LANGUAGES if len(lang) == 2}
|
||||
languages = {
|
||||
lang: native_languages.get(lang, display)
|
||||
for lang, display in settings.ALL_LANGUAGES
|
||||
if lang in self.transcripts
|
||||
}
|
||||
|
||||
if self.sub:
|
||||
languages['en'] = 'English'
|
||||
native_languages = {lang: label for lang, label in settings.LANGUAGES if len(lang) == 2}
|
||||
languages = {
|
||||
lang: native_languages.get(lang, display)
|
||||
for lang, display in settings.ALL_LANGUAGES
|
||||
if lang in other_lang
|
||||
}
|
||||
if not other_lang or (other_lang and sub):
|
||||
languages['en'] = 'English'
|
||||
|
||||
# OrderedDict for easy testing of rendered context in tests
|
||||
sorted_languages = sorted(languages.items(), key=itemgetter(1))
|
||||
if 'table' in self.transcripts:
|
||||
if 'table' in other_lang:
|
||||
sorted_languages.insert(0, ('table', 'Table of Contents'))
|
||||
|
||||
sorted_languages = OrderedDict(sorted_languages)
|
||||
@@ -233,7 +245,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
|
||||
elif self.html5_sources:
|
||||
download_video_link = self.html5_sources[0]
|
||||
|
||||
track_url, transcript_language, sorted_languages = self.get_transcripts_for_student()
|
||||
track_url, transcript_language, sorted_languages = self.get_transcripts_for_student(self.get_transcripts_info())
|
||||
|
||||
# CDN_VIDEO_URLS is only to be used here and will be deleted
|
||||
# TODO(ali@edx.org): Delete this after the CDN experiment has completed.
|
||||
@@ -250,42 +262,73 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
|
||||
cdn_eval = False
|
||||
cdn_exp_group = None
|
||||
|
||||
return self.system.render_template('video.html', {
|
||||
'ajax_url': self.system.ajax_url + '/save_user_state',
|
||||
self.youtube_streams = youtube_streams or create_youtube_string(self) # pylint: disable=W0201
|
||||
metadata = {
|
||||
'saveStateUrl': self.system.ajax_url + '/save_user_state',
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
|
||||
'streams': self.youtube_streams,
|
||||
'sub': self.sub,
|
||||
'sources': sources,
|
||||
|
||||
# This won't work when we move to data that
|
||||
# isn't on the filesystem
|
||||
'captionDataDir': getattr(self, 'data_dir', None),
|
||||
|
||||
'showCaptions': json.dumps(self.show_captions),
|
||||
'generalSpeed': self.global_speed,
|
||||
'speed': self.speed,
|
||||
'savedVideoPosition': self.saved_video_position.total_seconds(),
|
||||
'start': self.start_time.total_seconds(),
|
||||
'end': self.end_time.total_seconds(),
|
||||
'transcriptLanguage': transcript_language,
|
||||
'transcriptLanguages': sorted_languages,
|
||||
|
||||
# TODO: Later on the value 1500 should be taken from some global
|
||||
# configuration setting field.
|
||||
'ytTestTimeout': 1500,
|
||||
|
||||
'ytApiUrl': settings.YOUTUBE['API'],
|
||||
'ytTestUrl': settings.YOUTUBE['TEST_URL'],
|
||||
'transcriptTranslationUrl': self.runtime.handler_url(
|
||||
self, 'transcript', 'translation/__lang__'
|
||||
).rstrip('/?'),
|
||||
'transcriptAvailableTranslationsUrl': self.runtime.handler_url(
|
||||
self, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
|
||||
## For now, the option "data-autohide-html5" is hard coded. This option
|
||||
## either enables or disables autohiding of controls and captions on mouse
|
||||
## inactivity. If set to true, controls and captions will autohide for
|
||||
## HTML5 sources (non-YouTube) after a period of mouse inactivity over the
|
||||
## whole video. When the mouse moves (or a key is pressed while any part of
|
||||
## the video player is focused), the captions and controls will be shown
|
||||
## once again.
|
||||
##
|
||||
## There is no option in the "Advanced Editor" to set this option. However,
|
||||
## this option will have an effect if changed to "True". The code on
|
||||
## front-end exists.
|
||||
'autohideHtml5': False
|
||||
}
|
||||
|
||||
bumperize(self)
|
||||
|
||||
context = {
|
||||
'bumper_metadata': json.dumps(self.bumper['metadata']), # pylint: disable=E1101
|
||||
'metadata': json.dumps(OrderedDict(metadata)),
|
||||
'poster': json.dumps(get_poster(self)),
|
||||
'branding_info': branding_info,
|
||||
'cdn_eval': cdn_eval,
|
||||
'cdn_exp_group': cdn_exp_group,
|
||||
# This won't work when we move to data that
|
||||
# isn't on the filesystem
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
'display_name': self.display_name_with_default,
|
||||
'end': self.end_time.total_seconds(),
|
||||
'handout': self.handout,
|
||||
'id': self.location.html_id(),
|
||||
'show_captions': json.dumps(self.show_captions),
|
||||
'display_name': self.display_name_with_default,
|
||||
'handout': self.handout,
|
||||
'download_video_link': download_video_link,
|
||||
'sources': json.dumps(sources),
|
||||
'speed': json.dumps(self.speed),
|
||||
'general_speed': self.global_speed,
|
||||
'saved_video_position': self.saved_video_position.total_seconds(),
|
||||
'start': self.start_time.total_seconds(),
|
||||
'sub': self.sub,
|
||||
'track': track_url,
|
||||
'youtube_streams': youtube_streams or create_youtube_string(self),
|
||||
# TODO: Later on the value 1500 should be taken from some global
|
||||
# configuration setting field.
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_api_url': settings.YOUTUBE['API'],
|
||||
'yt_test_url': settings.YOUTUBE['TEST_URL'],
|
||||
'transcript_download_format': transcript_download_format,
|
||||
'transcript_download_formats_list': self.descriptor.fields['transcript_download_format'].values,
|
||||
'transcript_language': transcript_language,
|
||||
'transcript_languages': json.dumps(sorted_languages),
|
||||
'transcript_translation_url': self.runtime.handler_url(self, 'transcript', 'translation').rstrip('/?'),
|
||||
'transcript_available_translations_url': self.runtime.handler_url(self, 'transcript', 'available_translations').rstrip('/?'),
|
||||
'license': getattr(self, "license", None),
|
||||
})
|
||||
}
|
||||
return self.system.render_template('video.html', context)
|
||||
|
||||
|
||||
@XBlock.wants("settings")
|
||||
@@ -670,7 +713,10 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
|
||||
def _update_transcript_for_index(language=None):
|
||||
""" Find video transcript - if not found, don't update index """
|
||||
try:
|
||||
transcript = self.get_transcript(transcript_format='txt', lang=language)[0].replace("\n", " ")
|
||||
transcripts = self.get_transcripts_info()
|
||||
transcript = self.get_transcript(
|
||||
transcripts, transcript_format='txt', lang=language
|
||||
)[0].replace("\n", " ")
|
||||
transcript_index_name = "transcript_{}".format(language if language else self.transcript_language)
|
||||
video_body.update({transcript_index_name: transcript})
|
||||
except NotFoundError:
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
Module contains utils specific for video_module but not for transcripts.
|
||||
"""
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
import urllib
|
||||
import requests
|
||||
from urllib import urlencode
|
||||
from urlparse import parse_qs, urlsplit, urlunsplit
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
@@ -71,3 +76,40 @@ def get_video_from_cdn(cdn_base_url, original_video_url):
|
||||
return cdn_content['sources'][0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_poster(video):
|
||||
"""
|
||||
Generate poster metadata.
|
||||
|
||||
youtube_streams is string that contains '1.00:youtube_id'
|
||||
|
||||
Poster metadata is dict of youtube url for image thumbnail and edx logo
|
||||
"""
|
||||
if not video.bumper.get("enabled"):
|
||||
return
|
||||
|
||||
poster = OrderedDict({"url": "", "type": ""})
|
||||
|
||||
if video.youtube_streams:
|
||||
youtube_id = video.youtube_streams.split('1.00:')[1].split(',')[0]
|
||||
poster["url"] = settings.YOUTUBE['IMAGE_API'].format(youtube_id=youtube_id)
|
||||
poster["type"] = "youtube"
|
||||
else:
|
||||
poster["url"] = "https://www.edx.org/sites/default/files/theme/edx-logo-header.png"
|
||||
poster["type"] = "html5"
|
||||
|
||||
return poster
|
||||
|
||||
|
||||
def set_query_parameter(url, param_name, param_value):
|
||||
"""
|
||||
Given a URL, set or replace a query parameter and return the
|
||||
modified URL.
|
||||
"""
|
||||
scheme, netloc, path, query_string, fragment = urlsplit(url)
|
||||
query_params = parse_qs(query_string)
|
||||
query_params[param_name] = [param_value]
|
||||
new_query_string = urlencode(query_params, doseq=True)
|
||||
|
||||
return urlunsplit((scheme, netloc, path, new_query_string, fragment))
|
||||
|
||||
@@ -3,7 +3,7 @@ XFields for video module.
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from xblock.fields import Scope, String, Float, Boolean, List, Dict
|
||||
from xblock.fields import Scope, String, Float, Boolean, List, Dict, DateTime
|
||||
|
||||
from xmodule.fields import RelativeTime
|
||||
from xmodule.mixin import LicenseMixin
|
||||
@@ -142,7 +142,7 @@ class VideoFields(LicenseMixin):
|
||||
)
|
||||
speed = Float(
|
||||
help=_("The last speed that the user specified for the video."),
|
||||
scope=Scope.user_state,
|
||||
scope=Scope.user_state
|
||||
)
|
||||
global_speed = Float(
|
||||
help=_("The default speed for the video."),
|
||||
@@ -174,3 +174,12 @@ class VideoFields(LicenseMixin):
|
||||
scope=Scope.settings,
|
||||
default="",
|
||||
)
|
||||
bumper_last_view_date = DateTime(
|
||||
display_name=_("Date of the last view of the bumper"),
|
||||
scope=Scope.preferences,
|
||||
)
|
||||
bumper_do_not_show_again = Boolean(
|
||||
display_name=_("Do not show bumper again"),
|
||||
scope=Scope.preferences,
|
||||
default=False,
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ Video player in the courseware.
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
import requests
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from bok_choy.page_object import PageObject
|
||||
@@ -21,10 +22,12 @@ VIDEO_BUTTONS = {
|
||||
'download_transcript': '.video-tracks > a',
|
||||
'speed': '.speeds',
|
||||
'quality': '.quality-control',
|
||||
'do_not_show_again': '.skip-control',
|
||||
'skip_bumper': '.play-skip-control',
|
||||
}
|
||||
|
||||
CSS_CLASS_NAMES = {
|
||||
'closed_captions': '.closed .subtitles',
|
||||
'closed_captions': '.video.closed',
|
||||
'captions_rendered': '.video.is-captions-rendered',
|
||||
'captions': '.subtitles',
|
||||
'captions_text': '.subtitles > li',
|
||||
@@ -37,7 +40,8 @@ CSS_CLASS_NAMES = {
|
||||
'video_time': 'div.vidtime',
|
||||
'video_display_name': '.vert h2',
|
||||
'captions_lang_list': '.langs-list li',
|
||||
'video_speed': '.speeds .value'
|
||||
'video_speed': '.speeds .value',
|
||||
'poster': '.poster',
|
||||
}
|
||||
|
||||
VIDEO_MODES = {
|
||||
@@ -79,7 +83,7 @@ class VideoPage(PageObject):
|
||||
self.wait_for_element_presence(video_selector, 'Video is initialized')
|
||||
|
||||
@wait_for_js
|
||||
def wait_for_video_player_render(self):
|
||||
def wait_for_video_player_render(self, autoplay=False):
|
||||
"""
|
||||
Wait until Video Player Rendered Completely.
|
||||
|
||||
@@ -88,7 +92,12 @@ class VideoPage(PageObject):
|
||||
self.wait_for_element_presence(CSS_CLASS_NAMES['video_init'], 'Video Player Initialized')
|
||||
self.wait_for_element_presence(CSS_CLASS_NAMES['video_time'], 'Video Player Initialized')
|
||||
|
||||
video_player_buttons = ['volume', 'play', 'fullscreen', 'speed']
|
||||
video_player_buttons = ['volume', 'fullscreen', 'speed']
|
||||
if autoplay:
|
||||
video_player_buttons.append('pause')
|
||||
else:
|
||||
video_player_buttons.append('play')
|
||||
|
||||
for button in video_player_buttons:
|
||||
self.wait_for_element_visibility(VIDEO_BUTTONS[button], '{} button is visible'.format(button.title()))
|
||||
|
||||
@@ -106,6 +115,34 @@ class VideoPage(PageObject):
|
||||
|
||||
self.wait_for_ajax()
|
||||
|
||||
@wait_for_js
|
||||
def wait_for_video_bumper_render(self):
|
||||
"""
|
||||
Wait until Poster, Video Pre-Roll and main Video Player are Rendered Completely.
|
||||
"""
|
||||
self.wait_for_video_class()
|
||||
self.wait_for_element_presence(CSS_CLASS_NAMES['video_init'], 'Video Player Initialized')
|
||||
self.wait_for_element_presence(CSS_CLASS_NAMES['video_time'], 'Video Player Initialized')
|
||||
|
||||
video_player_buttons = ['do_not_show_again', 'skip_bumper', 'volume']
|
||||
for button in video_player_buttons:
|
||||
self.wait_for_element_visibility(VIDEO_BUTTONS[button], '{} button is visible'.format(button.title()))
|
||||
|
||||
@property
|
||||
def is_poster_shown(self):
|
||||
"""
|
||||
Check whether a poster is show.
|
||||
"""
|
||||
selector = self.get_element_selector(CSS_CLASS_NAMES['poster'])
|
||||
return self.q(css=selector).visible
|
||||
|
||||
def click_on_poster(self):
|
||||
"""
|
||||
Click on the video poster.
|
||||
"""
|
||||
selector = self.get_element_selector(CSS_CLASS_NAMES['poster'])
|
||||
self.q(css=selector).click()
|
||||
|
||||
def get_video_vertical_selector(self, video_display_name=None):
|
||||
"""
|
||||
Get selector for a video vertical with display name specified by `video_display_name`.
|
||||
@@ -184,19 +221,14 @@ class VideoPage(PageObject):
|
||||
@property
|
||||
def is_autoplay_enabled(self):
|
||||
"""
|
||||
Extract `data-autoplay` attribute to check video autoplay is enabled or disabled.
|
||||
Extract autoplay value of `data-metadata` attribute to check video autoplay is enabled or disabled.
|
||||
|
||||
Returns:
|
||||
bool: Tells if autoplay enabled/disabled.
|
||||
|
||||
"""
|
||||
selector = self.get_element_selector(CSS_CLASS_NAMES['video_container'])
|
||||
auto_play = self.q(css=selector).attrs('data-autoplay')[0]
|
||||
|
||||
if auto_play.lower() == 'false':
|
||||
return False
|
||||
|
||||
return True
|
||||
auto_play = json.loads(self.q(css=selector).attrs('data-metadata')[0])['autoplay']
|
||||
return auto_play
|
||||
|
||||
@property
|
||||
def is_error_message_shown(self):
|
||||
@@ -268,6 +300,7 @@ class VideoPage(PageObject):
|
||||
bool: True means captions are visible, False means captions are not visible
|
||||
|
||||
"""
|
||||
self.wait_for_ajax()
|
||||
caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
|
||||
return not self.q(css=caption_state_selector).present
|
||||
|
||||
@@ -515,6 +548,7 @@ class VideoPage(PageObject):
|
||||
|
||||
language_selector = VIDEO_MENUS["language"] + ' li[data-lang-code="{code}"]'.format(code=code)
|
||||
language_selector = self.get_element_selector(language_selector)
|
||||
|
||||
self.wait_for_element_visibility(language_selector, 'language menu is visible')
|
||||
self.q(css=language_selector).first.click()
|
||||
|
||||
|
||||
@@ -200,4 +200,5 @@ class AdvancedSettingsPage(CoursePage):
|
||||
'social_sharing_url',
|
||||
'teams_configuration',
|
||||
'minimum_grade_credit',
|
||||
'video_bumper',
|
||||
]
|
||||
|
||||
@@ -2,15 +2,62 @@
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import ddt
|
||||
|
||||
from ..helpers import EventsTestMixin
|
||||
from .test_video_module import VideoBaseTest
|
||||
from ...pages.lms.video.video import _parse_time_str
|
||||
|
||||
from openedx.core.lib.tests.assertions.events import assert_event_matches, assert_events_equal
|
||||
from opaque_keys.edx.keys import UsageKey, CourseKey
|
||||
|
||||
|
||||
class VideoEventsTest(EventsTestMixin, VideoBaseTest):
|
||||
class VideoEventsTestMixin(EventsTestMixin, VideoBaseTest):
|
||||
"""
|
||||
Useful helper methods to test video player event emission.
|
||||
"""
|
||||
def assert_payload_contains_ids(self, video_event):
|
||||
"""
|
||||
Video events should all contain "id" and "code" attributes in their payload.
|
||||
|
||||
This function asserts that those fields are present and have correct values.
|
||||
"""
|
||||
video_descriptors = self.course_fixture.get_nested_xblocks(category='video')
|
||||
video_desc = video_descriptors[0]
|
||||
video_locator = UsageKey.from_string(video_desc.locator)
|
||||
|
||||
expected_event = {
|
||||
'event': {
|
||||
'id': video_locator.html_id(),
|
||||
'code': '3_yD_cEKoCk'
|
||||
}
|
||||
}
|
||||
self.assert_events_match([expected_event], [video_event])
|
||||
|
||||
def assert_valid_control_event_at_time(self, video_event, time_in_seconds):
|
||||
"""
|
||||
Video control events should contain valid ID fields and a valid "currentTime" field.
|
||||
|
||||
This function asserts that those fields are present and have correct values.
|
||||
"""
|
||||
current_time = json.loads(video_event['event'])['currentTime']
|
||||
self.assertAlmostEqual(current_time, time_in_seconds, delta=1)
|
||||
|
||||
def assert_field_type(self, event_dict, field, field_type):
|
||||
"""Assert that a particular `field` in the `event_dict` has a particular type"""
|
||||
self.assertIn(field, event_dict, '{0} not found in the root of the event'.format(field))
|
||||
self.assertTrue(
|
||||
isinstance(event_dict[field], field_type),
|
||||
'Expected "{key}" to be a "{field_type}", but it has the value "{value}" of type "{t}"'.format(
|
||||
key=field,
|
||||
value=event_dict[field],
|
||||
t=type(event_dict[field]),
|
||||
field_type=field_type,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class VideoEventsTest(VideoEventsTestMixin):
|
||||
""" Test video player event emission """
|
||||
|
||||
def test_video_control_events(self):
|
||||
@@ -47,33 +94,6 @@ class VideoEventsTest(EventsTestMixin, VideoBaseTest):
|
||||
assert_event_matches({'event_type': 'pause_video'}, video_event)
|
||||
self.assert_valid_control_event_at_time(video_event, self.video.seconds)
|
||||
|
||||
def assert_payload_contains_ids(self, video_event):
|
||||
"""
|
||||
Video events should all contain "id" and "code" attributes in their payload.
|
||||
|
||||
This function asserts that those fields are present and have correct values.
|
||||
"""
|
||||
video_descriptors = self.course_fixture.get_nested_xblocks(category='video')
|
||||
video_desc = video_descriptors[0]
|
||||
video_locator = UsageKey.from_string(video_desc.locator)
|
||||
|
||||
expected_event = {
|
||||
'event': {
|
||||
'id': video_locator.html_id(),
|
||||
'code': '3_yD_cEKoCk'
|
||||
}
|
||||
}
|
||||
self.assert_events_match([expected_event], [video_event])
|
||||
|
||||
def assert_valid_control_event_at_time(self, video_event, time_in_seconds):
|
||||
"""
|
||||
Video control events should contain valid ID fields and a valid "currentTime" field.
|
||||
|
||||
This function asserts that those fields are present and have correct values.
|
||||
"""
|
||||
current_time = json.loads(video_event['event'])['currentTime']
|
||||
self.assertAlmostEqual(current_time, time_in_seconds, delta=1)
|
||||
|
||||
def test_strict_event_format(self):
|
||||
"""
|
||||
This test makes a very strong assertion about the fields present in events. The goal of it is to ensure that new
|
||||
@@ -127,15 +147,197 @@ class VideoEventsTest(EventsTestMixin, VideoBaseTest):
|
||||
}
|
||||
assert_events_equal(static_fields_pattern, load_video_event)
|
||||
|
||||
def assert_field_type(self, event_dict, field, field_type):
|
||||
"""Assert that a particular `field` in the `event_dict` has a particular type"""
|
||||
self.assertIn(field, event_dict, '{0} not found in the root of the event'.format(field))
|
||||
self.assertTrue(
|
||||
isinstance(event_dict[field], field_type),
|
||||
'Expected "{key}" to be a "{field_type}", but it has the value "{value}" of type "{t}"'.format(
|
||||
key=field,
|
||||
value=event_dict[field],
|
||||
t=type(event_dict[field]),
|
||||
field_type=field_type,
|
||||
)
|
||||
|
||||
@ddt.ddt
|
||||
class VideoBumperEventsTest(VideoEventsTestMixin):
|
||||
""" Test bumper video event emission """
|
||||
|
||||
# helper methods
|
||||
def watch_video_and_skip(self):
|
||||
"""
|
||||
Wait 5 seconds and press "skip" button.
|
||||
"""
|
||||
self.video.wait_for_position('0:05')
|
||||
self.video.click_player_button('skip_bumper')
|
||||
|
||||
def watch_video_and_dismiss(self):
|
||||
"""
|
||||
Wait 5 seconds and press "do not show again" button.
|
||||
"""
|
||||
self.video.wait_for_position('0:05')
|
||||
self.video.click_player_button('do_not_show_again')
|
||||
|
||||
def wait_for_state(self, state='finished'):
|
||||
"""
|
||||
Wait until video will be in given state.
|
||||
|
||||
Finished state means that video is played to the end.
|
||||
"""
|
||||
self.video.wait_for_state(state)
|
||||
|
||||
def add_bumper(self):
|
||||
"""
|
||||
Add video bumper to the course.
|
||||
"""
|
||||
additional_data = {
|
||||
u'video_bumper': {
|
||||
u'value': {
|
||||
"transcripts": {},
|
||||
"video_id": "edx_video_id"
|
||||
}
|
||||
}
|
||||
}
|
||||
self.course_fixture.add_advanced_settings(additional_data)
|
||||
|
||||
@ddt.data(
|
||||
('edx.video.bumper.skipped', watch_video_and_skip),
|
||||
('edx.video.bumper.dismissed', watch_video_and_dismiss),
|
||||
('edx.video.bumper.stopped', wait_for_state)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_video_control_events(self, event_type, action):
|
||||
"""
|
||||
Scenario: Video component with pre-roll emits events correctly
|
||||
Given the course has a Video component in "Youtube" mode with pre-roll enabled
|
||||
And I click on the video poster
|
||||
And the pre-roll video start playing
|
||||
And I watch (5 seconds/5 seconds/to the end of) it
|
||||
And I click (skip/do not show again) video button
|
||||
|
||||
Then a "edx.video.bumper.loaded" event is emitted
|
||||
And a "edx.video.bumper.played" event is emitted
|
||||
And a "edx.video.bumper.skipped/dismissed/stopped" event is emitted
|
||||
And a "load_video" event is emitted
|
||||
And a "play_video" event is emitted
|
||||
"""
|
||||
|
||||
def is_video_event(event):
|
||||
"""Filter out anything other than the video events of interest"""
|
||||
return event['event_type'] in (
|
||||
'edx.video.bumper.loaded',
|
||||
'edx.video.bumper.played',
|
||||
'edx.video.bumper.skipped',
|
||||
'edx.video.bumper.dismissed',
|
||||
'edx.video.bumper.stopped',
|
||||
'load_video',
|
||||
'play_video',
|
||||
'pause_video'
|
||||
) and self.video.state != 'buffering'
|
||||
|
||||
captured_events = []
|
||||
self.add_bumper()
|
||||
with self.capture_events(is_video_event, number_of_matches=5, captured_events=captured_events):
|
||||
self.navigate_to_video_no_render()
|
||||
self.video.click_on_poster()
|
||||
self.video.wait_for_video_bumper_render()
|
||||
sources, duration = self.video.sources[0], self.video.duration
|
||||
action(self)
|
||||
|
||||
# Filter subsequent events that appear due to bufferisation: edx.video.bumper.played
|
||||
# As bumper does not emit pause event, we filter subsequent edx.video.bumper.played events from
|
||||
# the list, except first.
|
||||
filtered_events = []
|
||||
for video_event in captured_events:
|
||||
is_played_event = video_event['event_type'] == 'edx.video.bumper.played'
|
||||
appears_again = filtered_events and video_event['event_type'] == filtered_events[-1]['event_type']
|
||||
if is_played_event and appears_again:
|
||||
continue
|
||||
filtered_events.append(video_event)
|
||||
|
||||
for idx, video_event in enumerate(filtered_events):
|
||||
if idx < 3:
|
||||
self.assert_bumper_payload_contains_ids(video_event, sources, duration)
|
||||
else:
|
||||
self.assert_payload_contains_ids(video_event)
|
||||
|
||||
if idx == 0:
|
||||
assert_event_matches({'event_type': 'edx.video.bumper.loaded'}, video_event)
|
||||
elif idx == 1:
|
||||
assert_event_matches({'event_type': 'edx.video.bumper.played'}, video_event)
|
||||
self.assert_valid_control_event_at_time(video_event, 0)
|
||||
elif idx == 2:
|
||||
assert_event_matches({'event_type': event_type}, video_event)
|
||||
elif idx == 3:
|
||||
assert_event_matches({'event_type': 'load_video'}, video_event)
|
||||
elif idx == 4:
|
||||
assert_event_matches({'event_type': 'play_video'}, video_event)
|
||||
self.assert_valid_control_event_at_time(video_event, 0)
|
||||
|
||||
def assert_bumper_payload_contains_ids(self, video_event, sources, duration):
|
||||
"""
|
||||
Bumper video events should all contain "host_component_id", "bumper_id",
|
||||
"duration", "code" attributes in their payload.
|
||||
|
||||
This function asserts that those fields are present and have correct values.
|
||||
"""
|
||||
self.add_bumper()
|
||||
video_descriptors = self.course_fixture.get_nested_xblocks(category='video')
|
||||
video_desc = video_descriptors[0]
|
||||
video_locator = UsageKey.from_string(video_desc.locator)
|
||||
|
||||
expected_event = {
|
||||
'event': {
|
||||
'host_component_id': video_locator.html_id(),
|
||||
'bumper_id': sources,
|
||||
'duration': _parse_time_str(duration),
|
||||
'code': 'html5'
|
||||
}
|
||||
}
|
||||
self.assert_events_match([expected_event], [video_event])
|
||||
|
||||
def test_strict_event_format(self):
|
||||
"""
|
||||
This test makes a very strong assertion about the fields present in events. The goal of it is to ensure that new
|
||||
fields are not added to all events mistakenly. It should be the only existing test that is updated when new top
|
||||
level fields are added to all events.
|
||||
"""
|
||||
|
||||
captured_events = []
|
||||
self.add_bumper()
|
||||
filter_event = lambda e: e['event_type'] == 'edx.video.bumper.loaded'
|
||||
with self.capture_events(filter_event, captured_events=captured_events):
|
||||
self.navigate_to_video_no_render()
|
||||
self.video.click_on_poster()
|
||||
|
||||
load_video_event = captured_events[0]
|
||||
|
||||
# Validate the event payload
|
||||
sources, duration = self.video.sources[0], self.video.duration
|
||||
self.assert_bumper_payload_contains_ids(load_video_event, sources, duration)
|
||||
|
||||
# We cannot predict the value of these fields so we make weaker assertions about them
|
||||
dynamic_string_fields = (
|
||||
'accept_language',
|
||||
'agent',
|
||||
'host',
|
||||
'ip',
|
||||
'event',
|
||||
'session'
|
||||
)
|
||||
for field in dynamic_string_fields:
|
||||
self.assert_field_type(load_video_event, field, basestring)
|
||||
self.assertIn(field, load_video_event, '{0} not found in the root of the event'.format(field))
|
||||
del load_video_event[field]
|
||||
|
||||
# A weak assertion for the timestamp as well
|
||||
self.assert_field_type(load_video_event, 'time', datetime.datetime)
|
||||
del load_video_event['time']
|
||||
|
||||
# Note that all unpredictable fields have been deleted from the event at this point
|
||||
|
||||
course_key = CourseKey.from_string(self.course_id)
|
||||
static_fields_pattern = {
|
||||
'context': {
|
||||
'course_id': unicode(course_key),
|
||||
'org_id': course_key.org,
|
||||
'path': '/event',
|
||||
'user_id': self.user_info['user_id']
|
||||
},
|
||||
'event_source': 'browser',
|
||||
'event_type': 'edx.video.bumper.loaded',
|
||||
'username': self.user_info['username'],
|
||||
'page': self.browser.current_url,
|
||||
'referer': self.browser.current_url,
|
||||
'name': 'edx.video.bumper.loaded',
|
||||
}
|
||||
assert_events_equal(static_fields_pattern, load_video_event)
|
||||
|
||||
@@ -397,6 +397,7 @@ class YouTubeVideoTest(VideoBaseTest):
|
||||
'time_to_response': 2.0,
|
||||
'youtube_api_blocked': True,
|
||||
})
|
||||
|
||||
self.metadata = self.metadata_for_mode('youtube_html5')
|
||||
|
||||
self.navigate_to_video()
|
||||
@@ -711,6 +712,84 @@ class YouTubeVideoTest(VideoBaseTest):
|
||||
|
||||
self.assertEqual(self.video.caption_languages, {'zh_HANS': 'Simplified Chinese', 'zh_HANT': 'Traditional Chinese'})
|
||||
|
||||
def test_video_bumper_render(self):
|
||||
"""
|
||||
Scenario: Multiple videos with bumper in sequentials all load and work, switching between sequentials
|
||||
Given it has videos "A,B" in "Youtube" and "HTML5" modes in position "1" of sequential
|
||||
And video "C" in "Youtube" mode in position "2" of sequential
|
||||
When I open sequential position "1"
|
||||
Then I see video "B" has a poster
|
||||
When I click on it
|
||||
Then I see video bumper is playing
|
||||
When I skip the bumper
|
||||
Then I see the main video
|
||||
When I click on video "A"
|
||||
Then the main video starts playing
|
||||
When I open sequential position "2"
|
||||
And click on the poster
|
||||
Then the main video starts playing
|
||||
Then I see that the main video starts playing once I go back to position "2" of sequential
|
||||
When I reload the page
|
||||
Then I see that the main video starts playing when I click on the poster
|
||||
"""
|
||||
additional_data = {
|
||||
u'video_bumper': {
|
||||
u'value': {
|
||||
"transcripts": {},
|
||||
"video_id": "edx_video_id"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.verticals = [
|
||||
[{'display_name': 'A'}, {'display_name': 'B', 'metadata': self.metadata_for_mode('html5')}],
|
||||
[{'display_name': 'C'}]
|
||||
]
|
||||
|
||||
tab1_video_names = ['A', 'B']
|
||||
tab2_video_names = ['C']
|
||||
|
||||
def execute_video_steps(video_names):
|
||||
"""
|
||||
Execute video steps
|
||||
"""
|
||||
for video_name in video_names:
|
||||
self.video.use_video(video_name)
|
||||
self.assertTrue(self.video.is_poster_shown)
|
||||
self.video.click_on_poster()
|
||||
self.video.wait_for_video_player_render(autoplay=True)
|
||||
self.assertIn(self.video.state, ['playing', 'buffering', 'finished'])
|
||||
|
||||
self.course_fixture.add_advanced_settings(additional_data)
|
||||
self.navigate_to_video_no_render()
|
||||
|
||||
self.video.use_video('B')
|
||||
self.assertTrue(self.video.is_poster_shown)
|
||||
self.video.click_on_poster()
|
||||
self.video.wait_for_video_bumper_render()
|
||||
self.assertIn(self.video.state, ['playing', 'buffering', 'finished'])
|
||||
self.video.click_player_button('skip_bumper')
|
||||
|
||||
# no autoplay here, maybe video is too small, so pause is not switched
|
||||
self.video.wait_for_video_player_render()
|
||||
self.assertIn(self.video.state, ['playing', 'buffering', 'finished'])
|
||||
|
||||
self.video.use_video('A')
|
||||
execute_video_steps(['A'])
|
||||
|
||||
# go to second sequential position
|
||||
self.course_nav.go_to_sequential_position(2)
|
||||
|
||||
execute_video_steps(tab2_video_names)
|
||||
|
||||
# go back to first sequential position
|
||||
# we are again playing tab 1 videos to ensure that switching didn't broke some video functionality.
|
||||
self.course_nav.go_to_sequential_position(1)
|
||||
execute_video_steps(tab1_video_names)
|
||||
|
||||
self.video.browser.refresh()
|
||||
execute_video_steps(tab1_video_names)
|
||||
|
||||
|
||||
class YouTubeHtml5VideoTest(VideoBaseTest):
|
||||
""" Test YouTube HTML5 Video Player """
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,15 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Video xmodule tests in mongo."""
|
||||
|
||||
from mock import patch
|
||||
from nose.plugins.attrib import attr
|
||||
import os
|
||||
import freezegun
|
||||
import tempfile
|
||||
import textwrap
|
||||
import json
|
||||
from datetime import timedelta
|
||||
import ddt
|
||||
|
||||
from nose.plugins.attrib import attr
|
||||
from datetime import timedelta, datetime
|
||||
from webob import Request
|
||||
from mock import MagicMock, Mock
|
||||
from mock import MagicMock, Mock, patch
|
||||
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import contentstore
|
||||
@@ -26,6 +28,9 @@ from xmodule.video_module.transcripts_utils import (
|
||||
TranscriptsGenerationException,
|
||||
)
|
||||
|
||||
|
||||
TRANSCRIPT = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]}
|
||||
BUMPER_TRANSCRIPT = {"start": [1], "end": [10], "text": ["A bumper"]}
|
||||
SRT_content = textwrap.dedent("""
|
||||
0
|
||||
00:00:00,12 --> 00:00:00,100
|
||||
@@ -104,6 +109,20 @@ def _upload_file(subs_file, location, filename):
|
||||
del_cached_content(content.location)
|
||||
|
||||
|
||||
def attach_sub(item, filename):
|
||||
"""
|
||||
Attach `en` transcript.
|
||||
"""
|
||||
item.sub = filename
|
||||
|
||||
|
||||
def attach_bumper_transcript(item, filename, lang="en"):
|
||||
"""
|
||||
Attach bumper transcript.
|
||||
"""
|
||||
item.video_bumper["transcripts"][lang] = filename
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class TestVideo(BaseTestXmodule):
|
||||
"""Integration tests: web client + mongo."""
|
||||
@@ -129,6 +148,8 @@ class TestVideo(BaseTestXmodule):
|
||||
{'speed': 2.0},
|
||||
{'saved_video_position': "00:00:10"},
|
||||
{'transcript_language': 'uk'},
|
||||
{'bumper_do_not_show_again': True},
|
||||
{'bumper_last_view_date': True},
|
||||
{'demoo<EFBFBD>': 'sample'}
|
||||
]
|
||||
for sample in data:
|
||||
@@ -151,6 +172,15 @@ class TestVideo(BaseTestXmodule):
|
||||
self.item_descriptor.handle_ajax('save_user_state', {'transcript_language': "uk"})
|
||||
self.assertEqual(self.item_descriptor.transcript_language, 'uk')
|
||||
|
||||
self.assertEqual(self.item_descriptor.bumper_do_not_show_again, False)
|
||||
self.item_descriptor.handle_ajax('save_user_state', {'bumper_do_not_show_again': True})
|
||||
self.assertEqual(self.item_descriptor.bumper_do_not_show_again, True)
|
||||
|
||||
with freezegun.freeze_time(datetime.now()):
|
||||
self.assertEqual(self.item_descriptor.bumper_last_view_date, None)
|
||||
self.item_descriptor.handle_ajax('save_user_state', {'bumper_last_view_date': True})
|
||||
self.assertEqual(self.item_descriptor.bumper_last_view_date, datetime.utcnow())
|
||||
|
||||
response = self.item_descriptor.handle_ajax('save_user_state', {u'demoo<EFBFBD>': "sample"})
|
||||
self.assertEqual(json.loads(response)['success'], True)
|
||||
|
||||
@@ -166,7 +196,7 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo):
|
||||
|
||||
Tests for `available_translations` dispatch.
|
||||
"""
|
||||
non_en_file = _create_srt_file()
|
||||
srt_file = _create_srt_file()
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
@@ -175,7 +205,7 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo):
|
||||
<source src="example.webm"/>
|
||||
<transcript language="uk" src="{}"/>
|
||||
</video>
|
||||
""".format(os.path.split(non_en_file.name)[1])
|
||||
""".format(os.path.split(srt_file.name)[1])
|
||||
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
@@ -197,7 +227,7 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo):
|
||||
self.assertEqual(json.loads(response.body), ['en'])
|
||||
|
||||
def test_available_translation_non_en(self):
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
|
||||
_upload_file(self.srt_file, self.item_descriptor.location, os.path.split(self.srt_file.name)[1])
|
||||
|
||||
request = Request.blank('/available_translations')
|
||||
response = self.item.transcript(request=request, dispatch='available_translations')
|
||||
@@ -210,7 +240,7 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo):
|
||||
_upload_sjson_file(good_sjson, self.item_descriptor.location)
|
||||
|
||||
# Upload non-english transcript.
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
|
||||
_upload_file(self.srt_file, self.item_descriptor.location, os.path.split(self.srt_file.name)[1])
|
||||
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
|
||||
@@ -220,6 +250,63 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo):
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@ddt.ddt
|
||||
class TestTranscriptAvailableTranslationsBumperDispatch(TestVideo):
|
||||
"""
|
||||
Test video handler that provide available translations info.
|
||||
|
||||
Tests for `available_translations_bumper` dispatch.
|
||||
"""
|
||||
srt_file = _create_srt_file()
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
<transcript language="uk" src="{}"/>
|
||||
</video>
|
||||
""".format(os.path.split(srt_file.name)[1])
|
||||
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestTranscriptAvailableTranslationsBumperDispatch, self).setUp()
|
||||
self.item_descriptor.render(STUDENT_VIEW)
|
||||
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
self.dispatch = "available_translations/?is_bumper=1"
|
||||
self.item.video_bumper = {"transcripts": {"en": ""}}
|
||||
|
||||
@ddt.data("en", "uk")
|
||||
def test_available_translation_en_and_non_en(self, lang):
|
||||
filename = os.path.split(self.srt_file.name)[1]
|
||||
_upload_file(self.srt_file, self.item_descriptor.location, filename)
|
||||
self.item.video_bumper["transcripts"][lang] = filename
|
||||
|
||||
request = Request.blank('/' + self.dispatch)
|
||||
response = self.item.transcript(request=request, dispatch=self.dispatch)
|
||||
self.assertEqual(json.loads(response.body), [lang])
|
||||
|
||||
def test_multiple_available_translations(self):
|
||||
en_translation = _create_srt_file()
|
||||
en_translation_filename = os.path.split(en_translation.name)[1]
|
||||
uk_translation_filename = os.path.split(self.srt_file.name)[1]
|
||||
# Upload english transcript.
|
||||
_upload_file(en_translation, self.item_descriptor.location, en_translation_filename)
|
||||
|
||||
# Upload non-english transcript.
|
||||
_upload_file(self.srt_file, self.item_descriptor.location, uk_translation_filename)
|
||||
|
||||
self.item.video_bumper["transcripts"]["en"] = en_translation_filename
|
||||
self.item.video_bumper["transcripts"]["uk"] = uk_translation_filename
|
||||
|
||||
request = Request.blank('/' + self.dispatch)
|
||||
response = self.item.transcript(request=request, dispatch=self.dispatch)
|
||||
self.assertEqual(json.loads(response.body), ['en', 'uk'])
|
||||
|
||||
|
||||
class TestTranscriptDownloadDispatch(TestVideo):
|
||||
"""
|
||||
Test video handler that provide translation transcripts.
|
||||
@@ -272,8 +359,9 @@ class TestTranscriptDownloadDispatch(TestVideo):
|
||||
request = Request.blank('/download')
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
transcripts = self.item.get_transcripts_info()
|
||||
with self.assertRaises(NotFoundError):
|
||||
self.item.get_transcript()
|
||||
self.item.get_transcript(transcripts)
|
||||
|
||||
@patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', u"塞.srt", 'application/x-subrip; charset=utf-8'))
|
||||
def test_download_non_en_non_ascii_filename(self, __):
|
||||
@@ -285,14 +373,15 @@ class TestTranscriptDownloadDispatch(TestVideo):
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@ddt.ddt
|
||||
class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
"""
|
||||
Test video handler that provide translation transcripts.
|
||||
|
||||
Tests for `translation` dispatch.
|
||||
Tests for `translation` and `translation_bumper` dispatches.
|
||||
"""
|
||||
|
||||
non_en_file = _create_srt_file()
|
||||
srt_file = _create_srt_file()
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
@@ -301,7 +390,7 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
<source src="example.webm"/>
|
||||
<transcript language="uk" src="{}"/>
|
||||
</video>
|
||||
""".format(os.path.split(non_en_file.name)[1])
|
||||
""".format(os.path.split(srt_file.name)[1])
|
||||
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
@@ -311,37 +400,41 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
super(TestTranscriptTranslationGetDispatch, self).setUp()
|
||||
self.item_descriptor.render(STUDENT_VIEW)
|
||||
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
self.item.video_bumper = {"transcripts": {"en": ""}}
|
||||
|
||||
def test_translation_fails(self):
|
||||
@ddt.data(
|
||||
# No language
|
||||
request = Request.blank('/translation')
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
self.assertEqual(response.status, '400 Bad Request')
|
||||
|
||||
('/translation', 'translation', '400 Bad Request'),
|
||||
# No videoId - HTML5 video with language that is not in available languages
|
||||
request = Request.blank('/translation/ru')
|
||||
response = self.item.transcript(request=request, dispatch='translation/ru')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
|
||||
('/translation/ru', 'translation/ru', '404 Not Found'),
|
||||
# Language is not in available languages
|
||||
request = Request.blank('/translation/ru?videoId=12345')
|
||||
response = self.item.transcript(request=request, dispatch='translation/ru')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
|
||||
('/translation/ru?videoId=12345', 'translation/ru', '404 Not Found'),
|
||||
# Youtube_id is invalid or does not exist
|
||||
request = Request.blank('/translation/uk?videoId=9855256955511225')
|
||||
response = self.item.transcript(request=request, dispatch='translation/uk')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
('/translation/uk?videoId=9855256955511225', 'translation/uk', '404 Not Found'),
|
||||
('/translation?is_bumper=1', 'translation', '400 Bad Request'),
|
||||
('/translation/ru?is_bumper=1', 'translation/ru', '404 Not Found'),
|
||||
('/translation/ru?videoId=12345&is_bumper=1', 'translation/ru', '404 Not Found'),
|
||||
('/translation/uk?videoId=9855256955511225&is_bumper=1', 'translation/uk', '404 Not Found'),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_translation_fails(self, url, dispatch, status_code):
|
||||
request = Request.blank(url)
|
||||
response = self.item.transcript(request=request, dispatch=dispatch)
|
||||
self.assertEqual(response.status, status_code)
|
||||
|
||||
def test_translaton_en_youtube_success(self):
|
||||
@ddt.data(
|
||||
('translation/en?videoId={}', 'translation/en', attach_sub),
|
||||
('translation/en?videoId={}&is_bumper=1', 'translation/en', attach_bumper_transcript))
|
||||
@ddt.unpack
|
||||
def test_translaton_en_youtube_success(self, url, dispatch, attach):
|
||||
subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]}
|
||||
good_sjson = _create_file(json.dumps(subs))
|
||||
_upload_sjson_file(good_sjson, self.item_descriptor.location)
|
||||
subs_id = _get_subs_id(good_sjson.name)
|
||||
|
||||
self.item.sub = subs_id
|
||||
request = Request.blank('/translation/en?videoId={}'.format(subs_id))
|
||||
response = self.item.transcript(request=request, dispatch='translation/en')
|
||||
attach(self.item, subs_id)
|
||||
request = Request.blank(url.format(subs_id))
|
||||
response = self.item.transcript(request=request, dispatch=dispatch)
|
||||
self.assertDictEqual(json.loads(response.body), subs)
|
||||
|
||||
def test_translation_non_en_youtube_success(self):
|
||||
@@ -352,9 +445,9 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.'
|
||||
]
|
||||
}
|
||||
self.non_en_file.seek(0)
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
|
||||
subs_id = _get_subs_id(self.non_en_file.name)
|
||||
self.srt_file.seek(0)
|
||||
_upload_file(self.srt_file, self.item_descriptor.location, os.path.split(self.srt_file.name)[1])
|
||||
subs_id = _get_subs_id(self.srt_file.name)
|
||||
|
||||
# youtube 1_0 request, will generate for all speeds for existing ids
|
||||
self.item.youtube_id_1_0 = subs_id
|
||||
@@ -387,16 +480,19 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
}
|
||||
self.assertDictEqual(json.loads(response.body), calculated_1_5)
|
||||
|
||||
def test_translaton_en_html5_success(self):
|
||||
subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]}
|
||||
good_sjson = _create_file(json.dumps(subs))
|
||||
@ddt.data(
|
||||
('translation/en', 'translation/en', attach_sub),
|
||||
('translation/en?is_bumper=1', 'translation/en', attach_bumper_transcript))
|
||||
@ddt.unpack
|
||||
def test_translaton_en_html5_success(self, url, dispatch, attach):
|
||||
good_sjson = _create_file(json.dumps(TRANSCRIPT))
|
||||
_upload_sjson_file(good_sjson, self.item_descriptor.location)
|
||||
subs_id = _get_subs_id(good_sjson.name)
|
||||
|
||||
self.item.sub = subs_id
|
||||
request = Request.blank('/translation/en')
|
||||
response = self.item.transcript(request=request, dispatch='translation/en')
|
||||
self.assertDictEqual(json.loads(response.body), subs)
|
||||
attach(self.item, subs_id)
|
||||
request = Request.blank(url)
|
||||
response = self.item.transcript(request=request, dispatch=dispatch)
|
||||
self.assertDictEqual(json.loads(response.body), TRANSCRIPT)
|
||||
|
||||
def test_translaton_non_en_html5_success(self):
|
||||
subs = {
|
||||
@@ -406,8 +502,8 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.'
|
||||
]
|
||||
}
|
||||
self.non_en_file.seek(0)
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
|
||||
self.srt_file.seek(0)
|
||||
_upload_file(self.srt_file, self.item_descriptor.location, os.path.split(self.srt_file.name)[1])
|
||||
|
||||
# manually clean youtube_id_1_0, as it has default value
|
||||
self.item.youtube_id_1_0 = ""
|
||||
@@ -453,7 +549,22 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
response = self.item.transcript(request=request, dispatch='translation/uk')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
|
||||
def test_translation_static_transcript(self):
|
||||
@ddt.data(
|
||||
# Test youtube style en
|
||||
('/translation/en?videoId=12345', 'translation/en', '307 Temporary Redirect', '12345'),
|
||||
# Test html5 style en
|
||||
('/translation/en', 'translation/en', '307 Temporary Redirect', 'OEoXaMPEzfM', attach_sub),
|
||||
# Test different language to ensure we are just ignoring it since we can't
|
||||
# translate with static fallback
|
||||
('/translation/uk', 'translation/uk', '404 Not Found'),
|
||||
(
|
||||
'/translation/en?is_bumper=1', 'translation/en', '307 Temporary Redirect', 'OEoXaMPEzfM',
|
||||
attach_bumper_transcript
|
||||
),
|
||||
('/translation/uk?is_bumper=1', 'translation/uk', '404 Not Found'),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_translation_static_transcript(self, url, dispatch, status_code, sub=None, attach=None):
|
||||
"""
|
||||
Set course static_asset_path and ensure we get redirected to that path
|
||||
if it isn't found in the contentstore
|
||||
@@ -464,30 +575,16 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id):
|
||||
store.update_item(self.course, self.user.id)
|
||||
|
||||
# Test youtube style en
|
||||
request = Request.blank('/translation/en?videoId=12345')
|
||||
response = self.item.transcript(request=request, dispatch='translation/en')
|
||||
self.assertEqual(response.status, '307 Temporary Redirect')
|
||||
self.assertIn(
|
||||
('Location', '/static/dummy/static/subs_12345.srt.sjson'),
|
||||
response.headerlist
|
||||
)
|
||||
|
||||
# Test HTML5 video style
|
||||
self.item.sub = 'OEoXaMPEzfM'
|
||||
request = Request.blank('/translation/en')
|
||||
response = self.item.transcript(request=request, dispatch='translation/en')
|
||||
self.assertEqual(response.status, '307 Temporary Redirect')
|
||||
self.assertIn(
|
||||
('Location', '/static/dummy/static/subs_OEoXaMPEzfM.srt.sjson'),
|
||||
response.headerlist
|
||||
)
|
||||
|
||||
# Test different language to ensure we are just ignoring it since we can't
|
||||
# translate with static fallback
|
||||
request = Request.blank('/translation/uk')
|
||||
response = self.item.transcript(request=request, dispatch='translation/uk')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
if attach:
|
||||
attach(self.item, sub)
|
||||
request = Request.blank(url)
|
||||
response = self.item.transcript(request=request, dispatch=dispatch)
|
||||
self.assertEqual(response.status, status_code)
|
||||
if sub:
|
||||
self.assertIn(
|
||||
('Location', '/static/dummy/static/subs_{}.srt.sjson'.format(sub)),
|
||||
response.headerlist
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@@ -497,7 +594,7 @@ class TestStudioTranscriptTranslationGetDispatch(TestVideo):
|
||||
|
||||
Tests for `translation` dispatch GET HTTP method.
|
||||
"""
|
||||
non_en_file = _create_srt_file()
|
||||
srt_file = _create_srt_file()
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
@@ -507,7 +604,7 @@ class TestStudioTranscriptTranslationGetDispatch(TestVideo):
|
||||
<transcript language="uk" src="{}"/>
|
||||
<transcript language="zh" src="{}"/>
|
||||
</video>
|
||||
""".format(os.path.split(non_en_file.name)[1], u"塞.srt".encode('utf8'))
|
||||
""".format(os.path.split(srt_file.name)[1], u"塞.srt".encode('utf8'))
|
||||
|
||||
MODEL_DATA = {'data': DATA}
|
||||
|
||||
@@ -523,12 +620,12 @@ class TestStudioTranscriptTranslationGetDispatch(TestVideo):
|
||||
self.assertEqual(response.status, '400 Bad Request')
|
||||
|
||||
# Correct case:
|
||||
filename = os.path.split(self.non_en_file.name)[1]
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, filename)
|
||||
self.non_en_file.seek(0)
|
||||
filename = os.path.split(self.srt_file.name)[1]
|
||||
_upload_file(self.srt_file, self.item_descriptor.location, filename)
|
||||
self.srt_file.seek(0)
|
||||
request = Request.blank(u'translation/uk?filename={}'.format(filename))
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
self.assertEqual(response.body, self.non_en_file.read())
|
||||
self.assertEqual(response.body, self.srt_file.read())
|
||||
self.assertEqual(response.headers['Content-Type'], 'application/x-subrip; charset=utf-8')
|
||||
self.assertEqual(
|
||||
response.headers['Content-Disposition'],
|
||||
@@ -537,12 +634,12 @@ class TestStudioTranscriptTranslationGetDispatch(TestVideo):
|
||||
self.assertEqual(response.headers['Content-Language'], 'uk')
|
||||
|
||||
# Non ascii file name download:
|
||||
self.non_en_file.seek(0)
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, u'塞.srt')
|
||||
self.non_en_file.seek(0)
|
||||
self.srt_file.seek(0)
|
||||
_upload_file(self.srt_file, self.item_descriptor.location, u'塞.srt')
|
||||
self.srt_file.seek(0)
|
||||
request = Request.blank('translation/zh?filename={}'.format(u'塞.srt'.encode('utf8')))
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/zh')
|
||||
self.assertEqual(response.body, self.non_en_file.read())
|
||||
self.assertEqual(response.body, self.srt_file.read())
|
||||
self.assertEqual(response.headers['Content-Type'], 'application/x-subrip; charset=utf-8')
|
||||
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename="塞.srt"')
|
||||
self.assertEqual(response.headers['Content-Language'], 'zh')
|
||||
@@ -614,7 +711,7 @@ class TestGetTranscript(TestVideo):
|
||||
"""
|
||||
Make sure that `get_transcript` method works correctly
|
||||
"""
|
||||
non_en_file = _create_srt_file()
|
||||
srt_file = _create_srt_file()
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
@@ -624,7 +721,7 @@ class TestGetTranscript(TestVideo):
|
||||
<transcript language="uk" src="{}"/>
|
||||
<transcript language="zh" src="{}"/>
|
||||
</video>
|
||||
""".format(os.path.split(non_en_file.name)[1], u"塞.srt".encode('utf8'))
|
||||
""".format(os.path.split(srt_file.name)[1], u"塞.srt".encode('utf8'))
|
||||
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
@@ -660,7 +757,8 @@ class TestGetTranscript(TestVideo):
|
||||
_upload_sjson_file(good_sjson, self.item.location)
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
|
||||
text, filename, mime_type = self.item.get_transcript()
|
||||
transcripts = self.item.get_transcripts_info()
|
||||
text, filename, mime_type = self.item.get_transcript(transcripts)
|
||||
|
||||
expected_text = textwrap.dedent("""\
|
||||
0
|
||||
@@ -697,7 +795,8 @@ class TestGetTranscript(TestVideo):
|
||||
|
||||
_upload_sjson_file(good_sjson, self.item.location)
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
text, filename, mime_type = self.item.get_transcript("txt")
|
||||
transcripts = self.item.get_transcripts_info()
|
||||
text, filename, mime_type = self.item.get_transcript(transcripts, transcript_format="txt")
|
||||
expected_text = textwrap.dedent("""\
|
||||
Hi, welcome to Edx.
|
||||
Let's start with what is on your screen right now.""")
|
||||
@@ -708,14 +807,15 @@ class TestGetTranscript(TestVideo):
|
||||
|
||||
def test_en_with_empty_sub(self):
|
||||
|
||||
transcripts = {"transcripts": {}, "sub": ""}
|
||||
# no self.sub, self.youttube_1_0 exist, but no file in assets
|
||||
with self.assertRaises(NotFoundError):
|
||||
self.item.get_transcript()
|
||||
self.item.get_transcript(transcripts)
|
||||
|
||||
# no self.sub and no self.youtube_1_0
|
||||
# no self.sub and no self.youtube_1_0, no non-en transcritps
|
||||
self.item.youtube_id_1_0 = None
|
||||
with self.assertRaises(ValueError):
|
||||
self.item.get_transcript()
|
||||
self.item.get_transcript(transcripts)
|
||||
|
||||
# no self.sub but youtube_1_0 exists with file in assets
|
||||
good_sjson = _create_file(content=textwrap.dedent("""\
|
||||
@@ -737,7 +837,7 @@ class TestGetTranscript(TestVideo):
|
||||
_upload_sjson_file(good_sjson, self.item.location)
|
||||
self.item.youtube_id_1_0 = _get_subs_id(good_sjson.name)
|
||||
|
||||
text, filename, mime_type = self.item.get_transcript()
|
||||
text, filename, mime_type = self.item.get_transcript(transcripts)
|
||||
expected_text = textwrap.dedent("""\
|
||||
0
|
||||
00:00:00,270 --> 00:00:02,720
|
||||
@@ -755,10 +855,11 @@ class TestGetTranscript(TestVideo):
|
||||
|
||||
def test_non_en_with_non_ascii_filename(self):
|
||||
self.item.transcript_language = 'zh'
|
||||
self.non_en_file.seek(0)
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, u"塞.srt")
|
||||
self.srt_file.seek(0)
|
||||
_upload_file(self.srt_file, self.item_descriptor.location, u"塞.srt")
|
||||
|
||||
text, filename, mime_type = self.item.get_transcript()
|
||||
transcripts = self.item.get_transcripts_info()
|
||||
text, filename, mime_type = self.item.get_transcript(transcripts)
|
||||
expected_text = textwrap.dedent("""
|
||||
0
|
||||
00:00:00,12 --> 00:00:00,100
|
||||
@@ -774,8 +875,9 @@ class TestGetTranscript(TestVideo):
|
||||
_upload_sjson_file(good_sjson, self.item.location)
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
|
||||
transcripts = self.item.get_transcripts_info()
|
||||
with self.assertRaises(ValueError):
|
||||
self.item.get_transcript()
|
||||
self.item.get_transcript(transcripts)
|
||||
|
||||
def test_key_error(self):
|
||||
good_sjson = _create_file(content="""
|
||||
@@ -794,5 +896,6 @@ class TestGetTranscript(TestVideo):
|
||||
_upload_sjson_file(good_sjson, self.item.location)
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
|
||||
transcripts = self.item.get_transcripts_info()
|
||||
with self.assertRaises(KeyError):
|
||||
self.item.get_transcript()
|
||||
self.item.get_transcript(transcripts)
|
||||
|
||||
@@ -9,8 +9,9 @@ from nose.plugins.attrib import attr
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from xmodule.video_module import create_youtube_string, VideoDescriptor
|
||||
from xmodule.video_module import VideoDescriptor, bumper_utils, video_utils
|
||||
from xmodule.x_module import STUDENT_VIEW
|
||||
from xmodule.tests.test_video import VideoDescriptorTestBase
|
||||
from xmodule.tests.test_import import DummySystem
|
||||
@@ -31,43 +32,51 @@ class TestVideoYouTube(TestVideo):
|
||||
def test_video_constructor(self):
|
||||
"""Make sure that all parameters extracted correctly from xml"""
|
||||
context = self.item_descriptor.render(STUDENT_VIEW).content
|
||||
sources = json.dumps([u'example.mp4', u'example.webm'])
|
||||
sources = [u'example.mp4', u'example.webm']
|
||||
|
||||
expected_context = {
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
|
||||
'branding_info': None,
|
||||
'license': None,
|
||||
'bumper_metadata': 'null',
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
'display_name': u'A Name',
|
||||
'end': 3610.0,
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'show_captions': 'true',
|
||||
'handout': None,
|
||||
'download_video_link': u'example.mp4',
|
||||
'sources': sources,
|
||||
'speed': 'null',
|
||||
'general_speed': 1.0,
|
||||
'start': 3603.0,
|
||||
'saved_video_position': 0.0,
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'handout': None,
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'metadata': json.dumps(OrderedDict({
|
||||
"saveStateUrl": self.item_descriptor.xmodule_runtime.ajax_url + "/save_user_state",
|
||||
"autoplay": False,
|
||||
"streams": "0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg",
|
||||
"sub": "a_sub_file.srt.sjson",
|
||||
"sources": sources,
|
||||
"captionDataDir": None,
|
||||
"showCaptions": "true",
|
||||
"generalSpeed": 1.0,
|
||||
"speed": None,
|
||||
"savedVideoPosition": 0.0,
|
||||
"start": 3603.0,
|
||||
"end": 3610.0,
|
||||
"transcriptLanguage": "en",
|
||||
"transcriptLanguages": OrderedDict({"en": "English", "uk": u"Українська"}),
|
||||
"ytTestTimeout": 1500,
|
||||
"ytApiUrl": "www.youtube.com/iframe_api",
|
||||
"ytTestUrl": "gdata.youtube.com/feeds/api/videos/",
|
||||
"transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation/__lang__'
|
||||
).rstrip('/?'),
|
||||
"transcriptAvailableTranslationsUrl": self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
"autohideHtml5": False,
|
||||
})),
|
||||
'track': None,
|
||||
'youtube_streams': create_youtube_string(self.item_descriptor),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_api_url': 'www.youtube.com/iframe_api',
|
||||
'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
|
||||
'transcript_download_format': 'srt',
|
||||
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
|
||||
'transcript_language': u'en',
|
||||
'transcript_languages': json.dumps(OrderedDict({"en": "English", "uk": u"Українська"})),
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation'
|
||||
).rstrip('/?'),
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
'transcript_download_formats_list': [
|
||||
{'display_name': 'SubRip (.srt) file', 'value': 'srt'},
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': 'null',
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
@@ -100,43 +109,51 @@ class TestVideoNonYouTube(TestVideo):
|
||||
the template generates an empty string for the YouTube streams.
|
||||
"""
|
||||
context = self.item_descriptor.render(STUDENT_VIEW).content
|
||||
sources = json.dumps([u'example.mp4', u'example.webm'])
|
||||
sources = [u'example.mp4', u'example.webm']
|
||||
|
||||
expected_context = {
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'branding_info': None,
|
||||
'license': None,
|
||||
'bumper_metadata': 'null',
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
'show_captions': 'true',
|
||||
'handout': None,
|
||||
'display_name': u'A Name',
|
||||
'download_video_link': u'example.mp4',
|
||||
'end': 3610.0,
|
||||
'handout': None,
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'sources': sources,
|
||||
'speed': 'null',
|
||||
'general_speed': 1.0,
|
||||
'start': 3603.0,
|
||||
'saved_video_position': 0.0,
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'metadata': json.dumps(OrderedDict({
|
||||
"saveStateUrl": self.item_descriptor.xmodule_runtime.ajax_url + "/save_user_state",
|
||||
"autoplay": False,
|
||||
"streams": "1.00:3_yD_cEKoCk",
|
||||
"sub": "a_sub_file.srt.sjson",
|
||||
"sources": sources,
|
||||
"captionDataDir": None,
|
||||
"showCaptions": "true",
|
||||
"generalSpeed": 1.0,
|
||||
"speed": None,
|
||||
"savedVideoPosition": 0.0,
|
||||
"start": 3603.0,
|
||||
"end": 3610.0,
|
||||
"transcriptLanguage": "en",
|
||||
"transcriptLanguages": OrderedDict({"en": "English"}),
|
||||
"ytTestTimeout": 1500,
|
||||
"ytApiUrl": "www.youtube.com/iframe_api",
|
||||
"ytTestUrl": "gdata.youtube.com/feeds/api/videos/",
|
||||
"transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation/__lang__'
|
||||
).rstrip('/?'),
|
||||
"transcriptAvailableTranslationsUrl": self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
"autohideHtml5": False,
|
||||
})),
|
||||
'track': None,
|
||||
'youtube_streams': '1.00:3_yD_cEKoCk',
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_api_url': 'www.youtube.com/iframe_api',
|
||||
'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
|
||||
'transcript_download_format': 'srt',
|
||||
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
|
||||
'transcript_language': u'en',
|
||||
'transcript_languages': '{"en": "English"}',
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation'
|
||||
).rstrip('/?'),
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?')
|
||||
'transcript_download_formats_list': [
|
||||
{'display_name': 'SubRip (.srt) file', 'value': 'srt'},
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': 'null',
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
@@ -157,6 +174,32 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
def setUp(self):
|
||||
super(TestGetHtmlMethod, self).setUp()
|
||||
self.setup_course()
|
||||
self.default_metadata_dict = OrderedDict({
|
||||
"saveStateUrl": "",
|
||||
"autoplay": settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
"streams": "1.00:3_yD_cEKoCk",
|
||||
"sub": "a_sub_file.srt.sjson",
|
||||
"sources": '[]',
|
||||
"captionDataDir": None,
|
||||
"showCaptions": "true",
|
||||
"generalSpeed": 1.0,
|
||||
"speed": None,
|
||||
"savedVideoPosition": 0.0,
|
||||
"start": 3603.0,
|
||||
"end": 3610.0,
|
||||
"transcriptLanguage": "en",
|
||||
"transcriptLanguages": OrderedDict({"en": "English"}),
|
||||
"ytTestTimeout": 1500,
|
||||
"ytApiUrl": "www.youtube.com/iframe_api",
|
||||
"ytTestUrl": "gdata.youtube.com/feeds/api/videos/",
|
||||
"transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation/__lang__'
|
||||
).rstrip('/?'),
|
||||
"transcriptAvailableTranslationsUrl": self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
"autohideHtml5": False,
|
||||
})
|
||||
|
||||
def test_get_html_track(self):
|
||||
SOURCE_XML = """
|
||||
@@ -209,36 +252,31 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
'transcripts': '<transcript language="uk" src="ukrainian.srt" />',
|
||||
},
|
||||
]
|
||||
sources = json.dumps([u'example.mp4', u'example.webm'])
|
||||
sources = [u'example.mp4', u'example.webm']
|
||||
|
||||
expected_context = {
|
||||
'branding_info': None,
|
||||
'license': None,
|
||||
'bumper_metadata': 'null',
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
'show_captions': 'true',
|
||||
'handout': None,
|
||||
'display_name': u'A Name',
|
||||
'download_video_link': u'example.mp4',
|
||||
'end': 3610.0,
|
||||
'id': None,
|
||||
'sources': sources,
|
||||
'start': 3603.0,
|
||||
'saved_video_position': 0.0,
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'speed': 'null',
|
||||
'general_speed': 1.0,
|
||||
'track': u'http://www.example.com/track',
|
||||
'youtube_streams': '1.00:3_yD_cEKoCk',
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_api_url': 'www.youtube.com/iframe_api',
|
||||
'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
|
||||
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
|
||||
'handout': None,
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'metadata': '',
|
||||
'track': None,
|
||||
'transcript_download_format': 'srt',
|
||||
'transcript_download_formats_list': [
|
||||
{'display_name': 'SubRip (.srt) file', 'value': 'srt'},
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': 'null',
|
||||
}
|
||||
|
||||
for data in cases:
|
||||
metadata = self.default_metadata_dict
|
||||
metadata['sources'] = sources
|
||||
DATA = SOURCE_XML.format(
|
||||
download_track=data['download_track'],
|
||||
track=data['track'],
|
||||
@@ -252,22 +290,29 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
).rstrip('/?')
|
||||
|
||||
context = self.item_descriptor.render(STUDENT_VIEW).content
|
||||
|
||||
expected_context.update({
|
||||
'transcript_download_format': None if self.item_descriptor.track and self.item_descriptor.download_track else 'srt',
|
||||
'transcript_languages': '{"en": "English"}' if not data['transcripts'] else json.dumps({"uk": u'Українська'}),
|
||||
'transcript_language': u'en' if not data['transcripts'] or data.get('sub') else u'uk',
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation'
|
||||
metadata.update({
|
||||
'transcriptLanguages': {"en": "English"} if not data['transcripts'] else {"uk": u'Українська'},
|
||||
'transcriptLanguage': u'en' if not data['transcripts'] or data.get('sub') else u'uk',
|
||||
'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation/__lang__'
|
||||
).rstrip('/?'),
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'track': track_url if data['expected_track_url'] == u'a_sub_file.srt.sjson' else data['expected_track_url'],
|
||||
'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'sub': data['sub'],
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
})
|
||||
expected_context.update({
|
||||
'transcript_download_format': (
|
||||
None if self.item_descriptor.track and self.item_descriptor.download_track else 'srt'
|
||||
),
|
||||
'track': (
|
||||
track_url if data['expected_track_url'] == u'a_sub_file.srt.sjson' else data['expected_track_url']
|
||||
),
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'metadata': json.dumps(metadata)
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
context,
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context),
|
||||
@@ -295,7 +340,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
""",
|
||||
'result': {
|
||||
'download_video_link': u'example_source.mp4',
|
||||
'sources': json.dumps([u'example.mp4', u'example.webm']),
|
||||
'sources': [u'example.mp4', u'example.webm'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -307,7 +352,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
""",
|
||||
'result': {
|
||||
'download_video_link': u'example.mp4',
|
||||
'sources': json.dumps([u'example.mp4', u'example.webm']),
|
||||
'sources': [u'example.mp4', u'example.webm'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -326,7 +371,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
<source src="example.webm"/>
|
||||
""",
|
||||
'result': {
|
||||
'sources': json.dumps([u'example.mp4', u'example.webm']),
|
||||
'sources': [u'example.mp4', u'example.webm'],
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -334,31 +379,21 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
initial_context = {
|
||||
'branding_info': None,
|
||||
'license': None,
|
||||
'bumper_metadata': 'null',
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
'show_captions': 'true',
|
||||
'handout': None,
|
||||
'display_name': u'A Name',
|
||||
'download_video_link': None,
|
||||
'end': 3610.0,
|
||||
'id': None,
|
||||
'sources': '[]',
|
||||
'speed': 'null',
|
||||
'general_speed': 1.0,
|
||||
'start': 3603.0,
|
||||
'saved_video_position': 0.0,
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'download_video_link': u'example.mp4',
|
||||
'handout': None,
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'metadata': self.default_metadata_dict,
|
||||
'track': None,
|
||||
'youtube_streams': '1.00:3_yD_cEKoCk',
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_api_url': 'www.youtube.com/iframe_api',
|
||||
'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
|
||||
'transcript_download_format': 'srt',
|
||||
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
|
||||
'transcript_language': u'en',
|
||||
'transcript_languages': '{"en": "English"}',
|
||||
'transcript_download_formats_list': [
|
||||
{'display_name': 'SubRip (.srt) file', 'value': 'srt'},
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': 'null',
|
||||
}
|
||||
|
||||
for data in cases:
|
||||
@@ -371,17 +406,21 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
context = self.item_descriptor.render(STUDENT_VIEW).content
|
||||
|
||||
expected_context = dict(initial_context)
|
||||
expected_context.update({
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation'
|
||||
expected_context['metadata'].update({
|
||||
'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation/__lang__'
|
||||
).rstrip('/?'),
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'sources': data['result'].get('sources', []),
|
||||
})
|
||||
expected_context.update({
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'download_video_link': data['result'].get('download_video_link'),
|
||||
'metadata': json.dumps(expected_context['metadata'])
|
||||
})
|
||||
expected_context.update(data['result'])
|
||||
|
||||
self.assertEqual(
|
||||
context,
|
||||
@@ -413,7 +452,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
'edx_video_id': "meow",
|
||||
'result': {
|
||||
'download_video_link': u'example_source.mp4',
|
||||
'sources': json.dumps([u'example.mp4', u'example.webm']),
|
||||
'sources': [u'example.mp4', u'example.webm'],
|
||||
}
|
||||
}
|
||||
DATA = SOURCE_XML.format(
|
||||
@@ -469,39 +508,32 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
'result': {
|
||||
'download_video_link': None,
|
||||
# make sure the desktop_mp4 url is included as part of the alternative sources.
|
||||
'sources': json.dumps([u'example.mp4', u'example.webm', u'http://www.meowmix.com']),
|
||||
'sources': [u'example.mp4', u'example.webm', u'http://www.meowmix.com'],
|
||||
}
|
||||
}
|
||||
|
||||
# Video found for edx_video_id
|
||||
metadata = self.default_metadata_dict
|
||||
metadata['autoplay'] = False
|
||||
metadata['sources'] = ""
|
||||
initial_context = {
|
||||
'branding_info': None,
|
||||
'license': None,
|
||||
'bumper_metadata': 'null',
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
'show_captions': 'true',
|
||||
'handout': None,
|
||||
'display_name': u'A Name',
|
||||
'download_video_link': None,
|
||||
'end': 3610.0,
|
||||
'id': None,
|
||||
'sources': '[]',
|
||||
'speed': 'null',
|
||||
'general_speed': 1.0,
|
||||
'start': 3603.0,
|
||||
'saved_video_position': 0.0,
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'download_video_link': u'example.mp4',
|
||||
'handout': None,
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'track': None,
|
||||
'youtube_streams': '1.00:3_yD_cEKoCk',
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_api_url': 'www.youtube.com/iframe_api',
|
||||
'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
|
||||
'transcript_download_format': 'srt',
|
||||
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
|
||||
'transcript_language': u'en',
|
||||
'transcript_languages': '{"en": "English"}',
|
||||
'transcript_download_formats_list': [
|
||||
{'display_name': 'SubRip (.srt) file', 'value': 'srt'},
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': 'null',
|
||||
'metadata': metadata
|
||||
}
|
||||
|
||||
DATA = SOURCE_XML.format(
|
||||
@@ -514,17 +546,21 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
context = self.item_descriptor.render(STUDENT_VIEW).content
|
||||
|
||||
expected_context = dict(initial_context)
|
||||
expected_context.update({
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation'
|
||||
expected_context['metadata'].update({
|
||||
'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation/__lang__'
|
||||
).rstrip('/?'),
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'sources': data['result']['sources'],
|
||||
})
|
||||
expected_context.update({
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'download_video_link': data['result']['download_video_link'],
|
||||
'metadata': json.dumps(expected_context['metadata'])
|
||||
})
|
||||
expected_context.update(data['result'])
|
||||
|
||||
self.assertEqual(
|
||||
context,
|
||||
@@ -579,42 +615,32 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
'result': {
|
||||
'download_video_link': u'http://fake-video.edx.org/thundercats.mp4',
|
||||
# make sure the urls for the various encodings are included as part of the alternative sources.
|
||||
'sources': json.dumps(
|
||||
[u'example.mp4', u'example.webm'] +
|
||||
[video['url'] for video in encoded_videos]
|
||||
),
|
||||
'sources': [u'example.mp4', u'example.webm'] +
|
||||
[video['url'] for video in encoded_videos],
|
||||
}
|
||||
}
|
||||
|
||||
# Video found for edx_video_id
|
||||
metadata = self.default_metadata_dict
|
||||
metadata['sources'] = ""
|
||||
initial_context = {
|
||||
'branding_info': None,
|
||||
'license': None,
|
||||
'bumper_metadata': 'null',
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
'show_captions': 'true',
|
||||
'handout': None,
|
||||
'display_name': u'A Name',
|
||||
'download_video_link': None,
|
||||
'end': 3610.0,
|
||||
'id': None,
|
||||
'sources': '[]',
|
||||
'speed': 'null',
|
||||
'general_speed': 1.0,
|
||||
'start': 3603.0,
|
||||
'saved_video_position': 0.0,
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'download_video_link': u'example.mp4',
|
||||
'handout': None,
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'track': None,
|
||||
'youtube_streams': '1.00:3_yD_cEKoCk',
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_api_url': 'www.youtube.com/iframe_api',
|
||||
'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
|
||||
'transcript_download_format': 'srt',
|
||||
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
|
||||
'transcript_language': u'en',
|
||||
'transcript_languages': '{"en": "English"}',
|
||||
'transcript_download_formats_list': [
|
||||
{'display_name': 'SubRip (.srt) file', 'value': 'srt'},
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': 'null',
|
||||
'metadata': metadata,
|
||||
}
|
||||
|
||||
DATA = SOURCE_XML.format(
|
||||
@@ -627,17 +653,21 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
context = self.item_descriptor.render(STUDENT_VIEW).content
|
||||
|
||||
expected_context = dict(initial_context)
|
||||
expected_context.update({
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation'
|
||||
expected_context['metadata'].update({
|
||||
'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation/__lang__'
|
||||
).rstrip('/?'),
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'sources': data['result']['sources'],
|
||||
})
|
||||
expected_context.update({
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'download_video_link': data['result']['download_video_link'],
|
||||
'metadata': json.dumps(expected_context['metadata'])
|
||||
})
|
||||
expected_context.update(data['result'])
|
||||
|
||||
self.assertEqual(
|
||||
context,
|
||||
@@ -690,12 +720,10 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
""",
|
||||
'result': {
|
||||
'download_video_link': u'example_source.mp4',
|
||||
'sources': json.dumps(
|
||||
[
|
||||
u'http://cdn_example.com/example.mp4',
|
||||
u'http://cdn_example.com/example.webm'
|
||||
]
|
||||
),
|
||||
'sources': [
|
||||
u'http://cdn_example.com/example.mp4',
|
||||
u'http://cdn_example.com/example.webm'
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -712,31 +740,21 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
'url': 'http://www.xuetangx.com'
|
||||
},
|
||||
'license': None,
|
||||
'bumper_metadata': 'null',
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
'show_captions': 'true',
|
||||
'handout': None,
|
||||
'display_name': u'A Name',
|
||||
'download_video_link': None,
|
||||
'end': 3610.0,
|
||||
'handout': None,
|
||||
'id': None,
|
||||
'sources': '[]',
|
||||
'speed': 'null',
|
||||
'general_speed': 1.0,
|
||||
'start': 3603.0,
|
||||
'saved_video_position': 0.0,
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'metadata': self.default_metadata_dict,
|
||||
'track': None,
|
||||
'youtube_streams': '1.00:3_yD_cEKoCk',
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_api_url': 'www.youtube.com/iframe_api',
|
||||
'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
|
||||
'transcript_download_format': 'srt',
|
||||
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
|
||||
'transcript_language': u'en',
|
||||
'transcript_languages': '{"en": "English"}',
|
||||
'transcript_download_formats_list': [
|
||||
{'display_name': 'SubRip (.srt) file', 'value': 'srt'},
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': 'null',
|
||||
}
|
||||
|
||||
for data in cases:
|
||||
@@ -748,21 +766,23 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
)
|
||||
self.initialize_module(data=DATA)
|
||||
self.item_descriptor.xmodule_runtime.user_location = 'CN'
|
||||
|
||||
context = self.item_descriptor.render('student_view').content
|
||||
|
||||
expected_context = dict(initial_context)
|
||||
expected_context.update({
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation'
|
||||
expected_context['metadata'].update({
|
||||
'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation/__lang__'
|
||||
).rstrip('/?'),
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'sources': data['result'].get('sources', []),
|
||||
})
|
||||
expected_context.update({
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'download_video_link': data['result'].get('download_video_link'),
|
||||
'metadata': json.dumps(expected_context['metadata'])
|
||||
})
|
||||
expected_context.update(data['result'])
|
||||
|
||||
self.assertEqual(
|
||||
context,
|
||||
@@ -948,3 +968,125 @@ class VideoDescriptorTest(TestCase, VideoDescriptorTestBase):
|
||||
VideoDescriptor.from_xml(xml_data, module_system, id_generator=Mock())
|
||||
with self.assertRaises(ValVideoNotFoundError):
|
||||
get_video_info("test_edx_video_id")
|
||||
|
||||
|
||||
class TestVideoWithBumper(TestVideo):
|
||||
"""
|
||||
Tests rendered content in presence of video bumper.
|
||||
"""
|
||||
CATEGORY = "video"
|
||||
METADATA = {}
|
||||
FEATURES = settings.FEATURES
|
||||
|
||||
@patch('xmodule.video_module.bumper_utils.get_bumper_settings')
|
||||
def test_is_bumper_enabled(self, get_bumper_settings):
|
||||
"""
|
||||
Check that bumper is (not)shown if ENABLE_VIDEO_BUMPER is (False)True
|
||||
|
||||
Assume that bumper settings are correct.
|
||||
"""
|
||||
self.FEATURES.update({
|
||||
"SHOW_BUMPER_PERIODICITY": 1,
|
||||
"ENABLE_VIDEO_BUMPER": True,
|
||||
})
|
||||
|
||||
get_bumper_settings.return_value = {
|
||||
"video_id": "edx_video_id",
|
||||
"transcripts": {},
|
||||
}
|
||||
with override_settings(FEATURES=self.FEATURES):
|
||||
self.assertTrue(bumper_utils.is_bumper_enabled(self.item_descriptor))
|
||||
|
||||
self.FEATURES.update({"ENABLE_VIDEO_BUMPER": False})
|
||||
|
||||
with override_settings(FEATURES=self.FEATURES):
|
||||
self.assertFalse(bumper_utils.is_bumper_enabled(self.item_descriptor))
|
||||
|
||||
@patch('xmodule.video_module.bumper_utils.is_bumper_enabled')
|
||||
@patch('xmodule.video_module.bumper_utils.get_bumper_settings')
|
||||
@patch('edxval.api.get_urls_for_profiles')
|
||||
def test_bumper_metadata(self, get_url_for_profiles, get_bumper_settings, is_bumper_enabled):
|
||||
"""
|
||||
Test content with rendered bumper metadata.
|
||||
"""
|
||||
get_url_for_profiles.return_value = {
|
||||
"desktop_mp4": "http://test_bumper.mp4",
|
||||
"desktop_webm": "",
|
||||
}
|
||||
|
||||
get_bumper_settings.return_value = {
|
||||
"video_id": "edx_video_id",
|
||||
"transcripts": {},
|
||||
}
|
||||
|
||||
is_bumper_enabled.return_value = True
|
||||
|
||||
content = self.item_descriptor.render(STUDENT_VIEW).content
|
||||
sources = [u'example.mp4', u'example.webm']
|
||||
expected_context = {
|
||||
'branding_info': None,
|
||||
'license': None,
|
||||
'bumper_metadata': json.dumps(OrderedDict({
|
||||
'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
"showCaptions": "true",
|
||||
"sources": ["http://test_bumper.mp4"],
|
||||
'streams': '',
|
||||
"transcriptLanguage": "en",
|
||||
"transcriptLanguages": {"en": "English"},
|
||||
"transcriptTranslationUrl": video_utils.set_query_parameter(
|
||||
self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation/__lang__'
|
||||
).rstrip('/?'), 'is_bumper', 1
|
||||
),
|
||||
"transcriptAvailableTranslationsUrl": video_utils.set_query_parameter(
|
||||
self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?'), 'is_bumper', 1
|
||||
),
|
||||
})),
|
||||
'cdn_eval': False,
|
||||
'cdn_exp_group': None,
|
||||
'display_name': u'A Name',
|
||||
'download_video_link': u'example.mp4',
|
||||
'handout': None,
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
'metadata': json.dumps(OrderedDict({
|
||||
"saveStateUrl": self.item_descriptor.xmodule_runtime.ajax_url + "/save_user_state",
|
||||
"autoplay": False,
|
||||
"streams": "0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg",
|
||||
"sub": "a_sub_file.srt.sjson",
|
||||
"sources": sources,
|
||||
"captionDataDir": None,
|
||||
"showCaptions": "true",
|
||||
"generalSpeed": 1.0,
|
||||
"speed": None,
|
||||
"savedVideoPosition": 0.0,
|
||||
"start": 3603.0,
|
||||
"end": 3610.0,
|
||||
"transcriptLanguage": "en",
|
||||
"transcriptLanguages": OrderedDict({"en": "English", "uk": u"Українська"}),
|
||||
"ytTestTimeout": 1500,
|
||||
"ytApiUrl": "www.youtube.com/iframe_api",
|
||||
"ytTestUrl": "gdata.youtube.com/feeds/api/videos/",
|
||||
"transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'translation/__lang__'
|
||||
).rstrip('/?'),
|
||||
"transcriptAvailableTranslationsUrl": self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
"autohideHtml5": False,
|
||||
})),
|
||||
'track': None,
|
||||
'transcript_download_format': 'srt',
|
||||
'transcript_download_formats_list': [
|
||||
{'display_name': 'SubRip (.srt) file', 'value': 'srt'},
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': json.dumps(OrderedDict({
|
||||
"url": "http://img.youtube.com/vi/ZwkTiUPN0mg/0.jpg",
|
||||
"type": "youtube"
|
||||
}))
|
||||
}
|
||||
|
||||
expected_content = self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
|
||||
self.assertEqual(content, expected_content)
|
||||
|
||||
@@ -206,7 +206,8 @@ def video_summary(video_profiles, course_id, video_descriptor, request, local_ca
|
||||
size = default_encoded_video.get('file_size', 0)
|
||||
|
||||
# Transcripts...
|
||||
transcript_langs = video_descriptor.available_translations(verify_assets=False)
|
||||
transcripts_info = video_descriptor.get_transcripts_info()
|
||||
transcript_langs = video_descriptor.available_translations(transcripts_info, verify_assets=False)
|
||||
|
||||
transcripts = {
|
||||
lang: reverse(
|
||||
@@ -227,7 +228,7 @@ def video_summary(video_profiles, course_id, video_descriptor, request, local_ca
|
||||
"duration": duration,
|
||||
"size": size,
|
||||
"transcripts": transcripts,
|
||||
"language": video_descriptor.get_default_transcript_language(),
|
||||
"language": video_descriptor.get_default_transcript_language(transcripts_info),
|
||||
"encoded_videos": video_data.get('profiles')
|
||||
}
|
||||
ret.update(always_available_data)
|
||||
|
||||
@@ -119,7 +119,8 @@ class VideoTranscripts(generics.RetrieveAPIView):
|
||||
)
|
||||
try:
|
||||
video_descriptor = modulestore().get_item(usage_key)
|
||||
content, filename, mimetype = video_descriptor.get_transcript(lang=lang)
|
||||
transcripts = video_descriptor.get_transcripts_info()
|
||||
content, filename, mimetype = video_descriptor.get_transcript(transcripts, lang=lang)
|
||||
except (NotFoundError, ValueError, KeyError):
|
||||
raise Http404(u"Transcript not found for {}, lang: {}".format(block_id, lang))
|
||||
|
||||
|
||||
@@ -123,6 +123,11 @@ FEATURES['ENABLE_MAX_FAILED_LOGIN_ATTEMPTS'] = False
|
||||
FEATURES['SQUELCH_PII_IN_LOGS'] = False
|
||||
FEATURES['PREVENT_CONCURRENT_LOGINS'] = False
|
||||
FEATURES['ADVANCED_SECURITY'] = False
|
||||
|
||||
FEATURES['ENABLE_MOBILE_REST_API'] = True # Show video bumper in LMS
|
||||
FEATURES['ENABLE_VIDEO_BUMPER'] = True # Show video bumper in LMS
|
||||
FEATURES['SHOW_BUMPER_PERIODICITY'] = 1
|
||||
|
||||
PASSWORD_MIN_LENGTH = None
|
||||
PASSWORD_COMPLEXITY = {}
|
||||
|
||||
|
||||
@@ -392,6 +392,13 @@ FEATURES = {
|
||||
|
||||
# Teams feature
|
||||
'ENABLE_TEAMS': False,
|
||||
|
||||
# Show video bumper in LMS
|
||||
'ENABLE_VIDEO_BUMPER': False,
|
||||
|
||||
# How many seconds to show the bumper again, default is 7 days:
|
||||
'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600,
|
||||
|
||||
}
|
||||
|
||||
# Ignore static asset files on import which match this pattern
|
||||
@@ -1671,6 +1678,8 @@ YOUTUBE = {
|
||||
'v': 'set_youtube_id_of_11_symbols_here',
|
||||
},
|
||||
},
|
||||
|
||||
'IMAGE_API': 'http://img.youtube.com/vi/{youtube_id}/0.jpg', # /maxresdefault.jpg for 1920*1080
|
||||
}
|
||||
|
||||
################################### APPS ######################################
|
||||
|
||||
@@ -7,47 +7,9 @@
|
||||
<div
|
||||
id="video_${id}"
|
||||
class="video closed"
|
||||
|
||||
data-streams="${youtube_streams}"
|
||||
|
||||
% if sub:
|
||||
data-sub="${sub}"
|
||||
% endif
|
||||
% if autoplay:
|
||||
data-autoplay="${autoplay}"
|
||||
% endif
|
||||
|
||||
data-sources='${sources}'
|
||||
data-save-state-url="${ajax_url}"
|
||||
data-caption-data-dir="${data_dir}"
|
||||
data-show-captions="${show_captions}"
|
||||
data-general-speed="${general_speed}"
|
||||
data-speed="${speed}"
|
||||
data-saved-video-position="${saved_video_position}"
|
||||
data-start="${start}"
|
||||
data-end="${end}"
|
||||
data-transcript-language="${transcript_language}"
|
||||
data-transcript-languages='${transcript_languages}'
|
||||
data-autoplay="${autoplay}"
|
||||
data-yt-test-timeout="${yt_test_timeout}"
|
||||
data-yt-api-url="${yt_api_url}"
|
||||
data-yt-test-url="${yt_test_url}"
|
||||
data-transcript-translation-url="${transcript_translation_url}"
|
||||
data-transcript-available-translations-url="${transcript_available_translations_url}"
|
||||
|
||||
## For now, the option "data-autohide-html5" is hard coded. This option
|
||||
## either enables or disables autohiding of controls and captions on mouse
|
||||
## inactivity. If set to true, controls and captions will autohide for
|
||||
## HTML5 sources (non-YouTube) after a period of mouse inactivity over the
|
||||
## whole video. When the mouse moves (or a key is pressed while any part of
|
||||
## the video player is focused), the captions and controls will be shown
|
||||
## once again.
|
||||
##
|
||||
## There is no option in the "Advanced Editor" to set this option. However,
|
||||
## this option will have an effect if changed to "True". The code on
|
||||
## front-end exists.
|
||||
data-autohide-html5="False"
|
||||
|
||||
data-metadata='${metadata}'
|
||||
data-bumper-metadata='${bumper_metadata}'
|
||||
data-poster='${poster}'
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="focus_grabber first"></div>
|
||||
@@ -65,41 +27,13 @@
|
||||
</section>
|
||||
<div class="video-player-post"></div>
|
||||
<section class="video-controls is-hidden">
|
||||
<div class="slider" title="${_('Video position')}"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
<li><a class="video_control" href="#" title="${_('Play')}" role="button" aria-disabled="false"></a></li>
|
||||
<li><div class="vidtime">0:00 / 0:00</div></li>
|
||||
</ul>
|
||||
<div class="secondary-controls">
|
||||
<div class="speeds menu-container">
|
||||
<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 menu" role="menu"></ol>
|
||||
</div>
|
||||
<div class="volume">
|
||||
<a href="#" role="button" aria-disabled="false" title="${_('Volume')}" aria-label="${_('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>
|
||||
<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">${_('Turn off captions')}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
|
||||
<div class="secondary-controls"></div>
|
||||
</div>
|
||||
</section>
|
||||
<a class="nav-skip sr" id="before-transcript_${id}" href="#after-transcript_${id}">${_('Skip to end of transcript.')}</a>
|
||||
</article>
|
||||
|
||||
<ol id="transcript-captions" class="subtitles" tabindex="0" role="group" aria-label="${_('Activating an item in this group will spool the video to the corresponding time point. To skip transcript, go to previous item.')}">
|
||||
<li></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<a class="nav-skip sr" id="after-transcript_${id}" href="#before-transcript_${id}">${_('Go back to start of transcript.')}</a>
|
||||
@@ -116,8 +50,8 @@
|
||||
% if transcript_download_format:
|
||||
<a href="${track}">${_('Download transcript')}</a>
|
||||
<div class="a11y-menu-container">
|
||||
<a class="a11y-menu-button" href="#" title="${'.' + transcript_download_format}">${'.' + transcript_download_format}</a>
|
||||
<ol class="a11y-menu-list">
|
||||
<a class="a11y-menu-button" href="#" title="${'.' + transcript_download_format}" role="button" aria-disabled="false">${'.' + transcript_download_format}</a>
|
||||
<ol class="a11y-menu-list" role="menu">
|
||||
% for item in transcript_download_formats_list:
|
||||
% if item['value'] == transcript_download_format:
|
||||
<li class="a11y-menu-item active">
|
||||
@@ -126,7 +60,7 @@
|
||||
% endif
|
||||
## This is necessary so we don't scrape 'display_name' as a string.
|
||||
<% dname = item['display_name'] %>
|
||||
<a class="a11y-menu-item-link" href="#${item['value']}" title="${_(dname)}" data-value="${item['value']}">
|
||||
<a class="a11y-menu-item-link" href="#${item['value']}" title="${_(dname)}" data-value="${item['value']}" role="menuitem" aria-disabled="false">
|
||||
${_(dname)}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user