Add Video Bumper.
Fix n-click behaviour on poster. Fix unit tests. Fix handler for non_en lang for bumper. Add more tests. Fix docstrings. Fix pep8. Fix static redirection with bumper. Fix button in IE11. Add video_bumper field in bok_choy. Fix pylink violations. Update docstrings and some clean up. Rename edx_video_id in bumper tests. Fix too long lines in help text. Address ui comments. Fix bumper events. Refactor bumper-transcripts code, fix bugs, address comments. Squashed commits: Fix download transcript button. [74e0c8c] Fix quality [a759f33] Fix error, when sub contains extension. [b30755c] Revert "Add video files to host for transcripts." This reverts commit cf8a96bf84346e17b6ad57ad4cc6a27d7a9118cd. [36f038a] Add video files to host for transcripts. [23f1655] Fix pep8 and pyling issues. [0f1f9d2] Update acceptance test. [765a27d] Wait for ajax in captions. [8ae72a3] Fix logic. [063450f] Fix unit tests. [d1075fc] Fix handlers tests. [25d31ad] Update bumper_utils. [cb5f9df] Remove maxDiff. [8738b1a] Code cleanup. [87dbcb7] Fix issues with transcripts. [ec899de] Fix transcripts in serializers. [444b1fc] Fix transcripts typo. [d524cb5] Fix bumper. [f62cf22] Fix video mongo tests. [8f1b55a] Fix dispatches. [53bc308] Add more fixes. [d5e3723] Fix test_video_handlers and rename the method. [93efc23] Fix mobile tests. [740e2ae] Fix pep8 and pylint. [47cfb66] Address comments, add fixes. [4e499d9] Add fixes. [8353553] Add improvements. Updated dispatch values) . Use ddt in bumper handler tests. Move common metadata to single place. Fix style. Update docstring. Fix poster button. Improve bumper events. Fix test after rebase. Address comments. Download transcript: use def video lang, not bump. Renamed date_last_view_bumper to bumper_last_view_date. Rename do_not_show_again_bumper to bumper_... Address comments. Fix tests for download for en lang. Fix bumper logic. Update strings. Update resizer. Remove resizer. Fix unit tests. Add tests. Fix bumper events. Clean up tests. Fix pylint violations. Fix pep8 and pylint violations. Update docs and method names. Update events. Make /static/ prefix a must. Fix wrong code.
This commit is contained in:
@@ -78,6 +78,9 @@ class CourseMetadata(object):
|
||||
if not settings.FEATURES.get('ENABLE_TEAMS'):
|
||||
filtered_list.append('teams_configuration')
|
||||
|
||||
if not settings.FEATURES.get('ENABLE_VIDEO_BUMPER'):
|
||||
filtered_list.append('video_bumper')
|
||||
|
||||
return filtered_list
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -74,6 +74,9 @@ FEATURES['ENABLE_TEAMS'] = True
|
||||
# Enable custom content licensing
|
||||
FEATURES['LICENSING'] = True
|
||||
|
||||
FEATURES['ENABLE_MOBILE_REST_API'] = True # Enable video bumper in Studio
|
||||
FEATURES['ENABLE_VIDEO_BUMPER'] = True # Enable video bumper in Studio settings
|
||||
|
||||
########################### Entrance Exams #################################
|
||||
FEATURES['ENTRANCE_EXAMS'] = True
|
||||
|
||||
|
||||
@@ -163,6 +163,13 @@ FEATURES = {
|
||||
|
||||
# Teams feature
|
||||
'ENABLE_TEAMS': False,
|
||||
|
||||
# Show video bumper in Studio
|
||||
'ENABLE_VIDEO_BUMPER': False,
|
||||
|
||||
# How many seconds to show the bumper again, default is 7 days:
|
||||
'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600,
|
||||
|
||||
}
|
||||
|
||||
ENABLE_JASMINE = False
|
||||
@@ -645,6 +652,8 @@ YOUTUBE = {
|
||||
'v': 'set_youtube_id_of_11_symbols_here',
|
||||
},
|
||||
},
|
||||
|
||||
'IMAGE_API': 'http://img.youtube.com/vi/{youtube_id}/0.jpg', # /maxresdefault.jpg for 1920*1080
|
||||
}
|
||||
|
||||
############################# VIDEO UPLOAD PIPELINE #############################
|
||||
|
||||
@@ -22,7 +22,7 @@ $a11y--blue-s1: saturate($blue,15%);
|
||||
}
|
||||
|
||||
.a11y-menu-list {
|
||||
@extend %ui-depth1;
|
||||
@extend %ui-depth3;
|
||||
top: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -27,6 +27,23 @@ div.video {
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: video pre-roll state
|
||||
&.is-pre-roll {
|
||||
.slider {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
position: relative;
|
||||
&:before {
|
||||
display: block;
|
||||
content: "";
|
||||
width: 100%;
|
||||
padding-top: 55%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.tc-wrapper {
|
||||
@include clearfix();
|
||||
position: relative;
|
||||
@@ -169,6 +186,7 @@ div.video {
|
||||
}
|
||||
|
||||
object, iframe, video {
|
||||
display: block;
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -282,7 +300,7 @@ div.video {
|
||||
}
|
||||
}
|
||||
|
||||
ul.vcr {
|
||||
.vcr {
|
||||
float: left;
|
||||
list-style: none;
|
||||
margin: 0 lh() 0 0;
|
||||
@@ -293,49 +311,52 @@ div.video {
|
||||
font-size: em(14);
|
||||
}
|
||||
|
||||
li {
|
||||
.video_control {
|
||||
@extend %video-button;
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
background-image: url('../images/vcr.png');
|
||||
background-position: 15px 15px ;
|
||||
background-repeat: no-repeat;
|
||||
border-left: none;
|
||||
padding: 0 lh(.75);
|
||||
width: 14px;
|
||||
|
||||
a {
|
||||
@extend %video-button;
|
||||
background-image: url('../images/vcr.png');
|
||||
background-position: 15px 15px ;
|
||||
background-repeat: no-repeat;
|
||||
border-left: none;
|
||||
box-shadow: 1px 0 0 #555;
|
||||
padding: 0 lh(.75);
|
||||
width: 14px;
|
||||
|
||||
&:focus {
|
||||
@extend %ui-depth4;
|
||||
position: relative;
|
||||
outline: $white dotted thin;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
&: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.
|
||||
|
||||
@@ -149,6 +149,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
|
||||
@@ -1665,6 +1672,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