Commit 042b7d5f by polesye

Merge pull request #428 from edx/valera/video_alpha2_refactor

Valera/video alpha2 refactor
parents a628b62d 6ba5d472
......@@ -14,6 +14,16 @@ is enabled.
Studio: Added improvements to Course Creation: richer error messaging, tip
text, and fourth field for course run.
Blades: New features for VideoAlpha player:
1.) Controls are auto hidden after a delay of mouse inactivity - the full video
becomes visible.
2.) When captions (CC) button is pressed, captions stick (not auto hidden after
a delay of mouse inactivity). The video player size does not change - the video
is down-sized and placed in the middle of the black area.
3.) All source code of Video Alpha 2 is written in JavaScript. It is not a basic
conversion from CoffeeScript. The structure of the player has been changed.
4.) A lot of additional unit tests.
LMS: Added user preferences (arbitrary user/key/value tuples, for which
which user/key is unique) and a REST API for reading users and
preferences. Access to the REST API is restricted by use of the
......
......@@ -66,6 +66,7 @@
// xmodule
@import 'xmodule/modules/css/module-styles.scss';
@import 'xmodule/descriptors/css/module-styles.scss';
@import 'elements/xmodules'; // styling for Studio-specific contexts
@import 'shame'; // shame file - used for any bad-form/orphaned scss that knowingly violate edX FED architecture/standards (see - http://csswizardry.com/2013/04/shame-css/)
// studio - elements - xmodules
// ====================
// Video Alpha
.xmodule_VideoAlphaModule {
// display mode
&.xmodule_display {
// full screen
.video-controls .add-fullscreen {
display: none !important; // nasty, but needed to override the bad specificity of the xmodule css selectors
}
}
}
......@@ -2,7 +2,7 @@
margin-bottom: 30px;
}
div.video {
div.videoalpha {
@include clearfix();
background: #f3f3f3;
display: block;
......@@ -10,11 +10,30 @@ div.video {
padding: 12px;
border-radius: 5px;
div.tc-wrapper {
position: relative;
@include clearfix;
}
article.video-wrapper {
float: left;
margin-right: flex-gutter(9);
width: flex-grid(6, 9);
background-color: black;
position: relative;
div.video-player-pre {
height: 50px;
background-color: black;
}
div.video-player-post {
height: 50px;
background-color: black;
}
section.video-player {
height: 0;
overflow: hidden;
......@@ -52,10 +71,19 @@ div.video {
border-radius: 0;
border-top: 1px solid #000;
box-shadow: inset 0 1px 0 #eee, 0 1px 0 #555;
height: 7px;
position: absolute;
z-index: 1;
bottom: 100%;
left: 0;
right: 0;
height: 14px;
margin-left: -1px;
margin-right: -1px;
@include transition(height 2.0s ease-in-out 0s);
-webkit-transition: -webkit-transform 0.7s ease-in-out;
-moz-transition: -moz-transform 0.7s ease-in-out;
-ms-transition: -ms-transform 0.7s ease-in-out;
transition: transform 0.7s ease-in-out;
@include transform(scaleY(0.5) translate3d(0, 50%, 0));
div.ui-widget-header {
background: #777;
......@@ -66,14 +94,18 @@ div.video {
background: $pink url(../images/slider-handle.png) center center no-repeat;
background-size: 50%;
border: 1px solid darken($pink, 20%);
border-radius: 15px;
border-radius: 50%;
box-shadow: inset 0 1px 0 lighten($pink, 10%);
cursor: pointer;
height: 15px;
margin-left: -7px;
top: -4px;
@include transition(height 2.0s ease-in-out 0s, width 2.0s ease-in-out 0s);
width: 15px;
height: 20px;
margin-left: 0;
top: 0;
-webkit-transition: -webkit-transform 0.7s ease-in-out;
-moz-transition: -moz-transform 0.7s ease-in-out;
-ms-transition: -ms-transform 0.7s ease-in-out;
transition: transform 0.7s ease-in-out;
@include transform(scale(.7, 1.3) translate3d(-80%, -15%, 0));
width: 20px;
&:focus, &:hover {
background-color: lighten($pink, 10%);
......@@ -213,7 +245,7 @@ div.video {
// fix for now
ol.video_speeds {
box-shadow: inset 1px 0 0 #555, 0 3px 0 #444;
box-shadow: inset 1px 0 0 #555, 0 4px 0 #444;
@include transition(none);
background-color: #444;
border: 1px solid #000;
......@@ -221,7 +253,7 @@ div.video {
display: none;
opacity: 0.0;
position: absolute;
width: 133px;
width: 131px;
z-index: 10;
li {
......@@ -362,7 +394,7 @@ div.video {
border-right: 1px solid #000;
box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
color: #797979;
display: block;
display: none;
float: left;
line-height: 46px; //height of play pause buttons
margin-left: 0;
......@@ -387,7 +419,6 @@ div.video {
a.hide-subtitles {
background: url('../images/cc.png') center no-repeat;
color: #797979;
display: block;
float: left;
font-weight: 800;
......@@ -410,6 +441,10 @@ div.video {
&.off {
opacity: 0.7;
}
background-color: #444;
color: #fff;
text-decoration: none;
}
}
}
......@@ -420,15 +455,10 @@ div.video {
}
div.slider {
height: 14px;
margin-top: -7px;
@include transform(scaleY(1) translate3d(0, 0, 0));
a.ui-slider-handle {
border-radius: 20px;
height: 20px;
margin-left: -10px;
top: -4px;
width: 20px;
@include transform(scale(1) translate3d(-50%, -15%, 0));
}
}
}
......@@ -471,22 +501,53 @@ div.video {
article.video-wrapper {
width: flex-grid(9,9);
background-color: inherit;
}
article.video-wrapper section.video-controls.html5 {
bottom: 0px;
left: 0px;
right: 0px;
position: absolute;
z-index: 1;
}
article.video-wrapper section.video-controls div.secondary-controls a.hide-subtitles {
background-color: inherit;
color: #797979;
text-decoration: inherit;
}
article.video-wrapper div.video-player-pre, article.video-wrapper div.video-player-post {
height: 0px;
}
ol.subtitles {
width: 0;
height: 0;
width: 0;
height: 0;
}
ol.subtitles.html5 {
background-color: rgba(243, 243, 243, 0.8);
height: 100%;
position: absolute;
right: 0;
bottom: 0;
top: 0;
width: 275px;
padding: 0 20px;
z-index: 0;
}
}
&.fullscreen {
&.video-fullscreen {
background: rgba(#000, .95);
border: 0;
bottom: 0;
height: 100%;
left: 0;
margin: 0;
overflow: hidden;
padding: 0;
position: fixed;
top: 0;
......@@ -501,12 +562,22 @@ div.video {
}
}
article.video-wrapper div.video-player-pre, article.video-wrapper div.video-player-post {
height: 0px;
}
article.video-wrapper {
position: static;
}
div.tc-wrapper {
@include clearfix;
display: table;
width: 100%;
height: 100%;
position: static;
article.video-wrapper {
width: 100%;
display: table-cell;
......@@ -536,7 +607,7 @@ div.video {
background: rgba(#000, .8);
bottom: 0;
height: 100%;
max-height: 100%;
max-height: 460px;
max-width: flex-grid(3);
padding: lh();
position: fixed;
......
......@@ -3,19 +3,51 @@
<div id="example">
<div
id="video_id"
class="video"
class="videoalpha"
data-streams="0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId"
data-show-captions="true"
data-start=""
data-end=""
data-caption-asset-path="/static/subs/">
data-caption-asset-path="/static/subs/"
data-autoplay="False"
>
<div class="tc-wrapper">
<article class="video-wrapper">
<div class="video-player-pre"></div>
<section class="video-player">
<div id="id"></div>
</section>
<section class="video-controls"></section>
<div class="video-player-post"></div>
<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 href="#">
<h3>Speed</h3>
<p class="active"></p>
</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" title="HD">HD</a>
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
</div>
</div>
</section>
</article>
<ol class="subtitles"><li></li></ol>
</div>
</div>
</div>
......
<div class="course-content">
<div id="video_example">
<div id="example">
<div
id="video_id"
class="videoalpha"
data-show-captions="true"
data-start=""
data-end=""
data-caption-asset-path="/static/subs/"
data-sub="test_name_of_the_subtitles"
data-mp4-source="test_files/test.mp4"
data-webm-source="test_files/test.webm"
data-ogg-source="test_files/test.ogv"
data-autoplay="False"
>
<div class="tc-wrapper">
<article class="video-wrapper">
<div class="video-player-pre"></div>
<section class="video-player">
<div id="id"></div>
</section>
<div class="video-player-post"></div>
<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 href="#">
<h3>Speed</h3>
<p class="active"></p>
</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" title="HD">HD</a>
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
</div>
</div>
</section>
</article>
<ol class="subtitles"><li></li></ol>
</div>
</div>
</div>
</div>
</div>
\ No newline at end of file
......@@ -3,16 +3,17 @@
<div id="example">
<div
id="video_id"
class="video"
class="videoalpha"
data-show-captions="true"
data-start=""
data-end=""
data-caption-asset-path="/static/subs/"
data-sub="test_name_of_the_subtitles"
data-mp4-source="test.mp4"
data-webm-source="test.webm"
data-ogg-source="test.ogv"
>
data-mp4-source="test_files/test.mp4"
data-webm-source="test_files/test.webm"
data-ogg-source="test_files/test.ogv"
data-autoplay="False"
>
<div class="tc-wrapper">
<article class="video-wrapper">
<section class="video-player">
......@@ -20,6 +21,8 @@
</section>
<section class="video-controls"></section>
</article>
<ol class="subtitles"><li></li></ol>
</div>
</div>
</div>
......
<div class="course-content">
<div id="video_example">
<div id="example">
<div
id="video_id"
class="videoalpha"
data-streams="0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId"
data-show-captions="false"
data-start=""
data-end=""
data-caption-asset-path="/static/subs/"
data-autoplay="False"
>
<div class="tc-wrapper">
<article class="video-wrapper">
<section class="video-player">
<div id="id"></div>
</section>
<section class="video-controls"></section>
</article>
</div>
</div>
</div>
</div>
</div>
\ No newline at end of file
*.js
# Tests for videoalpha are written in pure JavaScript.
!videoalpha/*.js
......@@ -8,6 +8,55 @@ window.YT =
BUFFERING: 3
CUED: 5
window.STATUS = window.YT.PlayerState
oldAjaxWithPrefix = window.jQuery.ajaxWithPrefix
jasmine.stubbedCaption =
end: [3120, 6270, 8490, 21620, 24920, 25750, 27900, 34380, 35550, 40250]
start: [1180, 3120, 6270, 14910, 21620, 24920, 25750, 27900, 34380, 35550]
text: [
"MICHAEL CIMA: So let's do the first one here.",
"Vacancies, where do they come from?",
"Well, imagine a perfect crystal.",
"Now we know at any temperature other than absolute zero there's enough",
"energy going around that some atoms will have more energy",
"than others, right?",
"There's a distribution.",
"If I plot energy here and number, these atoms in the crystal will have a",
"distribution of energy.",
"And some will have quite a bit of energy, just for a moment."
]
# For our purposes, we need to make sure that the function $.ajaxWithPrefix
# does not fail when during tests a captions file is requested.
# It is originally defined in
#
# common/static/coffee/src/ajax_prefix.js
#
# We will replace it with a function that does:
#
# 1.) Return a hard coded captions object if the file name contains 'test_name_of_the_subtitles'.
# 2.) Behaves the same a as the origianl in all other cases.
window.jQuery.ajaxWithPrefix = (url, settings) ->
if not settings
settings = url
url = settings.url
success = settings.success
data = settings.data
if url.match(/test_name_of_the_subtitles/g) isnt null or url.match(/slowerSpeedYoutubeId/g) isnt null or url.match(/normalSpeedYoutubeId/g) isnt null
if window.jQuery.isFunction(success) is true
success jasmine.stubbedCaption
else if window.jQuery.isFunction(data) is true
data jasmine.stubbedCaption
else
oldAjaxWithPrefix.apply @, arguments
# Time waitsFor() should wait for before failing a test.
window.WAIT_TIMEOUT = 1000
jasmine.getFixtures().fixturesPath = 'xmodule/js/fixtures'
jasmine.stubbedMetadata =
......@@ -33,10 +82,6 @@ jasmine.fireEvent = (el, eventName) ->
else
el.fireEvent("on" + event.eventType, event)
jasmine.stubbedCaption =
start: [0, 10000, 20000, 30000]
text: ['Caption at 0', 'Caption at 10000', 'Caption at 20000', 'Caption at 30000']
jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50']
jasmine.stubRequests = ->
......@@ -78,7 +123,8 @@ jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) ->
if createPlayer
return new VideoPlayer(video: context.video)
jasmine.stubVideoPlayerAlpha = (context, enableParts, createPlayer=true, html5=false) ->
jasmine.stubVideoPlayerAlpha = (context, enableParts, html5=false) ->
console.log('stubVideoPlayerAlpha called')
suite = context.suite
currentPartName = suite.description while suite = suite.parentSuite
if html5 == false
......@@ -88,10 +134,8 @@ jasmine.stubVideoPlayerAlpha = (context, enableParts, createPlayer=true, html5=f
jasmine.stubRequests()
YT.Player = undefined
window.OldVideoPlayerAlpha = undefined
context.video = new VideoAlpha '#example', '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
jasmine.stubYoutubePlayer()
if createPlayer
return new VideoPlayerAlpha(video: context.video)
return new VideoAlpha '#example', '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
# Stub jQuery.cookie
......
......@@ -163,11 +163,11 @@ describe 'VideoCaption', ->
it 'return a correct caption index', ->
expect(@caption.search(0)).toEqual 0
expect(@caption.search(9999)).toEqual 0
expect(@caption.search(10000)).toEqual 1
expect(@caption.search(15000)).toEqual 1
expect(@caption.search(30000)).toEqual 3
expect(@caption.search(30001)).toEqual 3
expect(@caption.search(9999)).toEqual 2
expect(@caption.search(10000)).toEqual 2
expect(@caption.search(15000)).toEqual 3
expect(@caption.search(30000)).toEqual 7
expect(@caption.search(30001)).toEqual 7
describe 'play', ->
describe 'when the caption was not rendered', ->
......@@ -220,7 +220,7 @@ describe 'VideoCaption', ->
@caption.updatePlayTime 25.000
it 'search the caption based on time', ->
expect(@caption.currentIndex).toEqual 2
expect(@caption.currentIndex).toEqual 5
describe 'when the video speed is not 1.0x', ->
beforeEach ->
......@@ -228,7 +228,7 @@ describe 'VideoCaption', ->
@caption.updatePlayTime 25.000
it 'search the caption based on 1.0x speed', ->
expect(@caption.currentIndex).toEqual 1
expect(@caption.currentIndex).toEqual 3
describe 'when the index is not the same', ->
beforeEach ->
......@@ -240,10 +240,10 @@ describe 'VideoCaption', ->
expect($('.subtitles li[data-index=1]')).not.toHaveClass 'current'
it 'activate new caption', ->
expect($('.subtitles li[data-index=2]')).toHaveClass 'current'
expect($('.subtitles li[data-index=5]')).toHaveClass 'current'
it 'save new index', ->
expect(@caption.currentIndex).toEqual 2
expect(@caption.currentIndex).toEqual 5
it 'scroll caption to new position', ->
expect($.fn.scrollTo).toHaveBeenCalled()
......@@ -251,11 +251,11 @@ describe 'VideoCaption', ->
describe 'when the index is the same', ->
beforeEach ->
@caption.currentIndex = 1
$('.subtitles li[data-index=1]').addClass 'current'
$('.subtitles li[data-index=3]').addClass 'current'
@caption.updatePlayTime 15.000
it 'does not change current subtitle', ->
expect($('.subtitles li[data-index=1]')).toHaveClass 'current'
expect($('.subtitles li[data-index=3]')).toHaveClass 'current'
describe 'resize', ->
......@@ -322,18 +322,18 @@ describe 'VideoCaption', ->
describe 'when the video speed is 1.0x', ->
beforeEach ->
@caption.currentSpeed = '1.0'
$('.subtitles li[data-start="30000"]').trigger('click')
$('.subtitles li[data-start="27900"]').trigger('click')
it 'trigger seek event with the correct time', ->
expect(@time).toEqual 30.000
expect(@time).toEqual 28.000
describe 'when the video speed is not 1.0x', ->
beforeEach ->
@caption.currentSpeed = '0.75'
$('.subtitles li[data-start="30000"]').trigger('click')
$('.subtitles li[data-start="27900"]').trigger('click')
it 'trigger seek event with the correct time', ->
expect(@time).toEqual 40.000
expect(@time).toEqual 37.000
describe 'toggle', ->
beforeEach ->
......
describe 'VideoAlpha HTML5Video', ->
playbackRates = [0.75, 1.0, 1.25, 1.5]
STATUS = window.YT.PlayerState
playerVars =
controls: 0
wmode: 'transparent'
rel: 0
showinfo: 0
enablejsapi: 1
modestbranding: 1
html5: 1
file = window.location.href.replace(/\/common(.*)$/, '') + '/test_root/data/videoalpha/gizmo'
html5Sources =
mp4: "#{file}.mp4"
webm: "#{file}.webm"
ogg: "#{file}.ogv"
onReady = jasmine.createSpy 'onReady'
onStateChange = jasmine.createSpy 'onStateChange'
beforeEach ->
loadFixtures 'videoalpha_html5.html'
@el = $('#example').find('.video')
@player = new window.HTML5Video.Player @el,
playerVars: playerVars,
videoSources: html5Sources,
events:
onReady: onReady
onStateChange: onStateChange
@videoEl = @el.find('.video-player video').get(0)
it 'PlayerState', ->
expect(HTML5Video.PlayerState).toEqual STATUS
describe 'constructor', ->
it 'create an html5 video element', ->
expect(@el.find('.video-player div')).toContain 'video'
it 'check if sources are created in correct way', ->
sources = $(@videoEl).find('source')
videoTypes = []
videoSources = []
$.each html5Sources, (index, source) ->
videoTypes.push index
videoSources.push source
$.each sources, (index, source) ->
s = $(source)
expect($.inArray(s.attr('src'), videoSources)).not.toEqual -1
expect($.inArray(s.attr('type').replace('video/', ''), videoTypes))
.not.toEqual -1
it 'check if click event is handled on the player', ->
expect(@videoEl).toHandle 'click'
# NOTE: According to
#
# https://github.com/ariya/phantomjs/wiki/Supported-Web-Standards#unsupported-features
#
# Video and Audio (due to the nature of PhantomJS) are not supported. After discussion
# with William Daly, some tests are disabled (Jenkins uses phantomjs for running tests
# and those tests fail).
#
# During code review, please enable the test below (change "xdescribe" to "describe"
# to enable the test).
xdescribe 'events:', ->
beforeEach ->
spyOn(@player, 'callStateChangeCallback').andCallThrough()
describe 'click', ->
describe 'when player is paused', ->
beforeEach ->
spyOn(@videoEl, 'play').andCallThrough()
@player.playerState = STATUS.PAUSED
$(@videoEl).trigger('click')
it 'native play event was called', ->
expect(@videoEl.play).toHaveBeenCalled()
it 'player state was changed', ->
expect(@player.playerState).toBe STATUS.PLAYING
it 'callback was called', ->
expect(@player.callStateChangeCallback).toHaveBeenCalled()
describe 'when player is played', ->
beforeEach ->
spyOn(@videoEl, 'pause').andCallThrough()
@player.playerState = STATUS.PLAYING
$(@videoEl).trigger('click')
it 'native pause event was called', ->
expect(@videoEl.pause).toHaveBeenCalled()
it 'player state was changed', ->
expect(@player.playerState).toBe STATUS.PAUSED
it 'callback was called', ->
expect(@player.callStateChangeCallback).toHaveBeenCalled()
describe 'play', ->
beforeEach ->
spyOn(@videoEl, 'play').andCallThrough()
@player.playerState = STATUS.PAUSED
@videoEl.play()
it 'native event was called', ->
expect(@videoEl.play).toHaveBeenCalled()
it 'player state was changed', ->
waitsFor ( ->
@player.playerState != HTML5Video.PlayerState.PAUSED
), 'Player state should be changed', 1000
runs ->
expect(@player.playerState).toBe STATUS.PLAYING
it 'callback was called', ->
waitsFor ( ->
@player.playerState != STATUS.PAUSED
), 'Player state should be changed', 1000
runs ->
expect(@player.callStateChangeCallback).toHaveBeenCalled()
describe 'pause', ->
beforeEach ->
spyOn(@videoEl, 'pause').andCallThrough()
@videoEl.play()
@videoEl.pause()
it 'native event was called', ->
expect(@videoEl.pause).toHaveBeenCalled()
it 'player state was changed', ->
waitsFor ( ->
@player.playerState != STATUS.UNSTARTED
), 'Player state should be changed', 1000
runs ->
expect(@player.playerState).toBe STATUS.PAUSED
it 'callback was called', ->
waitsFor ( ->
@player.playerState != HTML5Video.PlayerState.UNSTARTED
), 'Player state should be changed', 1000
runs ->
expect(@player.callStateChangeCallback).toHaveBeenCalled()
describe 'canplay', ->
beforeEach ->
waitsFor ( ->
@player.playerState != STATUS.UNSTARTED
), 'Video cannot be played', 1000
it 'player state was changed', ->
runs ->
expect(@player.playerState).toBe STATUS.PAUSED
it 'end property was defined', ->
runs ->
expect(@player.end).not.toBeNull()
it 'start position was defined', ->
runs ->
expect(@videoEl.currentTime).toBe(@player.start)
it 'callback was called', ->
runs ->
expect(@player.config.events.onReady).toHaveBeenCalled()
describe 'ended', ->
beforeEach ->
waitsFor ( ->
@player.playerState != STATUS.UNSTARTED
), 'Video cannot be played', 1000
it 'player state was changed', ->
runs ->
jasmine.fireEvent @videoEl, "ended"
expect(@player.playerState).toBe STATUS.ENDED
it 'callback was called', ->
jasmine.fireEvent @videoEl, "ended"
expect(@player.callStateChangeCallback).toHaveBeenCalled()
describe 'timeupdate', ->
beforeEach ->
spyOn(@videoEl, 'pause').andCallThrough()
waitsFor ( ->
@player.playerState != STATUS.UNSTARTED
), 'Video cannot be played', 1000
it 'player should be paused', ->
runs ->
@player.end = 3
@videoEl.currentTime = 5
jasmine.fireEvent @videoEl, "timeupdate"
expect(@videoEl.pause).toHaveBeenCalled()
it 'end param should be re-defined', ->
runs ->
@player.end = 3
@videoEl.currentTime = 5
jasmine.fireEvent @videoEl, "timeupdate"
expect(@player.end).toBe @videoEl.duration
# NOTE: According to
#
# https://github.com/ariya/phantomjs/wiki/Supported-Web-Standards#unsupported-features
#
# Video and Audio (due to the nature of PhantomJS) are not supported. After discussion
# with William Daly, some tests are disabled (Jenkins uses phantomjs for running tests
# and those tests fail).
#
# During code review, please enable the test below (change "xdescribe" to "describe"
# to enable the test).
xdescribe 'methods:', ->
beforeEach ->
waitsFor ( ->
@volume = @videoEl.volume
@seek = @videoEl.currentTime
@player.playerState == STATUS.PAUSED
), 'Video cannot be played', 1000
it 'pauseVideo', ->
spyOn(@videoEl, 'pause').andCallThrough()
@player.pauseVideo()
expect(@videoEl.pause).toHaveBeenCalled()
describe 'seekTo', ->
it 'set new correct value', ->
runs ->
@player.seekTo(2)
expect(@videoEl.currentTime).toBe 2
it 'set new inccorrect values', ->
runs ->
@player.seekTo(-50)
expect(@videoEl.currentTime).toBe @seek
@player.seekTo('5')
expect(@videoEl.currentTime).toBe @seek
@player.seekTo(500000)
expect(@videoEl.currentTime).toBe @seek
describe 'setVolume', ->
it 'set new correct value', ->
runs ->
@player.setVolume(50)
expect(@videoEl.volume).toBe 50*0.01
it 'set new inccorrect values', ->
runs ->
@player.setVolume(-50)
expect(@videoEl.volume).toBe @volume
@player.setVolume('5')
expect(@videoEl.volume).toBe @volume
@player.setVolume(500000)
expect(@videoEl.volume).toBe @volume
it 'getCurrentTime', ->
runs ->
@videoEl.currentTime = 3
expect(@player.getCurrentTime()).toBe @videoEl.currentTime
it 'playVideo', ->
runs ->
spyOn(@videoEl, 'play').andCallThrough()
@player.playVideo()
expect(@videoEl.play).toHaveBeenCalled()
it 'getPlayerState', ->
runs ->
@player.playerState = STATUS.PLAYING
expect(@player.getPlayerState()).toBe STATUS.PLAYING
@player.playerState = STATUS.ENDED
expect(@player.getPlayerState()).toBe STATUS.ENDED
it 'getVolume', ->
runs ->
@volume = @videoEl.volume = 0.5
expect(@player.getVolume()).toBe @volume
it 'getDuration', ->
runs ->
@duration = @videoEl.duration
expect(@player.getDuration()).toBe @duration
describe 'setPlaybackRate', ->
it 'set a correct value', ->
@playbackRate = 1.5
@player.setPlaybackRate @playbackRate
expect(@videoEl.playbackRate).toBe @playbackRate
it 'set NaN value', ->
@playbackRate = NaN
@player.setPlaybackRate @playbackRate
expect(@videoEl.playbackRate).toBe 1.0
it 'getAvailablePlaybackRates', ->
expect(@player.getAvailablePlaybackRates()).toEqual playbackRates
describe 'VideoControlAlpha', ->
beforeEach ->
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
loadFixtures 'videoalpha.html'
$('.video-controls').html ''
describe 'constructor', ->
it 'render the video controls', ->
@control = new window.VideoControlAlpha(el: $('.video-controls'))
expect($('.video-controls')).toContain
['.slider', 'ul.vcr', 'a.play', '.vidtime', '.add-fullscreen'].join(',')
expect($('.video-controls').find('.vidtime')).toHaveText '0:00 / 0:00'
it 'bind the playback button', ->
@control = new window.VideoControlAlpha(el: $('.video-controls'))
expect($('.video_control')).toHandleWith 'click', @control.togglePlayback
describe 'when on a touch based device', ->
beforeEach ->
window.onTouchBasedDevice.andReturn true
@control = new window.VideoControlAlpha(el: $('.video-controls'))
it 'does not add the play class to video control', ->
expect($('.video_control')).not.toHaveClass 'play'
expect($('.video_control')).not.toHaveHtml 'Play'
describe 'when on a non-touch based device', ->
beforeEach ->
@control = new window.VideoControlAlpha(el: $('.video-controls'))
it 'add the play class to video control', ->
expect($('.video_control')).toHaveClass 'play'
expect($('.video_control')).toHaveHtml 'Play'
describe 'play', ->
beforeEach ->
@control = new window.VideoControlAlpha(el: $('.video-controls'))
@control.play()
it 'switch playback button to play state', ->
expect($('.video_control')).not.toHaveClass 'play'
expect($('.video_control')).toHaveClass 'pause'
expect($('.video_control')).toHaveHtml 'Pause'
describe 'pause', ->
beforeEach ->
@control = new window.VideoControlAlpha(el: $('.video-controls'))
@control.pause()
it 'switch playback button to pause state', ->
expect($('.video_control')).not.toHaveClass 'pause'
expect($('.video_control')).toHaveClass 'play'
expect($('.video_control')).toHaveHtml 'Play'
describe 'togglePlayback', ->
beforeEach ->
@control = new window.VideoControlAlpha(el: $('.video-controls'))
describe 'when the control does not have play or pause class', ->
beforeEach ->
$('.video_control').removeClass('play').removeClass('pause')
describe 'when the video is playing', ->
beforeEach ->
$('.video_control').addClass('play')
spyOnEvent @control, 'pause'
@control.togglePlayback jQuery.Event('click')
it 'does not trigger the pause event', ->
expect('pause').not.toHaveBeenTriggeredOn @control
describe 'when the video is paused', ->
beforeEach ->
$('.video_control').addClass('pause')
spyOnEvent @control, 'play'
@control.togglePlayback jQuery.Event('click')
it 'does not trigger the play event', ->
expect('play').not.toHaveBeenTriggeredOn @control
describe 'when the video is playing', ->
beforeEach ->
spyOnEvent @control, 'pause'
$('.video_control').addClass 'pause'
@control.togglePlayback jQuery.Event('click')
it 'trigger the pause event', ->
expect('pause').toHaveBeenTriggeredOn @control
describe 'when the video is paused', ->
beforeEach ->
spyOnEvent @control, 'play'
$('.video_control').addClass 'play'
@control.togglePlayback jQuery.Event('click')
it 'trigger the play event', ->
expect('play').toHaveBeenTriggeredOn @control
describe 'VideoProgressSliderAlpha', ->
beforeEach ->
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
describe 'constructor', ->
describe 'on a non-touch based device', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
@player = jasmine.stubVideoPlayerAlpha @
@progressSlider = @player.progressSlider
it 'build the slider', ->
expect(@progressSlider.slider).toBe '.slider'
expect($.fn.slider).toHaveBeenCalledWith
range: 'min'
change: @progressSlider.onChange
slide: @progressSlider.onSlide
stop: @progressSlider.onStop
it 'build the seek handle', ->
expect(@progressSlider.handle).toBe '.slider .ui-slider-handle'
expect($.fn.qtip).toHaveBeenCalledWith
content: "0:00"
position:
my: 'bottom center'
at: 'top center'
container: @progressSlider.handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
describe 'on a touch-based device', ->
beforeEach ->
window.onTouchBasedDevice.andReturn true
spyOn($.fn, 'slider').andCallThrough()
@player = jasmine.stubVideoPlayerAlpha @
@progressSlider = @player.progressSlider
it 'does not build the slider', ->
expect(@progressSlider.slider).toBeUndefined
expect($.fn.slider).not.toHaveBeenCalled()
describe 'play', ->
beforeEach ->
spyOn(VideoProgressSliderAlpha.prototype, 'buildSlider').andCallThrough()
@player = jasmine.stubVideoPlayerAlpha @
@progressSlider = @player.progressSlider
describe 'when the slider was already built', ->
beforeEach ->
@progressSlider.play()
it 'does not build the slider', ->
expect(@progressSlider.buildSlider.calls.length).toEqual 1
describe 'when the slider was not already built', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
@progressSlider.slider = null
@progressSlider.play()
it 'build the slider', ->
expect(@progressSlider.slider).toBe '.slider'
expect($.fn.slider).toHaveBeenCalledWith
range: 'min'
change: @progressSlider.onChange
slide: @progressSlider.onSlide
stop: @progressSlider.onStop
it 'build the seek handle', ->
expect(@progressSlider.handle).toBe '.ui-slider-handle'
expect($.fn.qtip).toHaveBeenCalledWith
content: "0:00"
position:
my: 'bottom center'
at: 'top center'
container: @progressSlider.handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
describe 'updatePlayTime', ->
beforeEach ->
@player = jasmine.stubVideoPlayerAlpha @
@progressSlider = @player.progressSlider
describe 'when frozen', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
@progressSlider.frozen = true
@progressSlider.updatePlayTime 20, 120
it 'does not update the slider', ->
expect($.fn.slider).not.toHaveBeenCalled()
describe 'when not frozen', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
@progressSlider.frozen = false
@progressSlider.updatePlayTime 20, 120
it 'update the max value of the slider', ->
expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 120
it 'update current value of the slider', ->
expect($.fn.slider).toHaveBeenCalledWith 'value', 20
describe 'onSlide', ->
beforeEach ->
@player = jasmine.stubVideoPlayerAlpha @
@progressSlider = @player.progressSlider
spyOnEvent @progressSlider, 'slide_seek'
@progressSlider.onSlide {}, value: 20
it 'freeze the slider', ->
expect(@progressSlider.frozen).toBeTruthy()
it 'update the tooltip', ->
expect($.fn.qtip).toHaveBeenCalled()
it 'trigger seek event', ->
expect('slide_seek').toHaveBeenTriggeredOn @progressSlider
expect(@player.currentTime).toEqual 20
describe 'onChange', ->
beforeEach ->
@player = jasmine.stubVideoPlayerAlpha @
@progressSlider = @player.progressSlider
@progressSlider.onChange {}, value: 20
it 'update the tooltip', ->
expect($.fn.qtip).toHaveBeenCalled()
describe 'onStop', ->
beforeEach ->
@player = jasmine.stubVideoPlayerAlpha @
@progressSlider = @player.progressSlider
spyOnEvent @progressSlider, 'slide_seek'
@progressSlider.onStop {}, value: 20
it 'freeze the slider', ->
expect(@progressSlider.frozen).toBeTruthy()
it 'trigger seek event', ->
expect('slide_seek').toHaveBeenTriggeredOn @progressSlider
expect(@player.currentTime).toEqual 20
it 'set timeout to unfreeze the slider', ->
expect(window.setTimeout).toHaveBeenCalledWith jasmine.any(Function), 200
window.setTimeout.mostRecentCall.args[0]()
expect(@progressSlider.frozen).toBeFalsy()
describe 'updateTooltip', ->
beforeEach ->
@player = jasmine.stubVideoPlayerAlpha @
@progressSlider = @player.progressSlider
@progressSlider.updateTooltip 90
it 'set the tooltip value', ->
expect($.fn.qtip).toHaveBeenCalledWith 'option', 'content.text', '1:30'
describe 'VideoSpeedControlAlpha', ->
beforeEach ->
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
jasmine.stubVideoPlayerAlpha @
$('.speeds').remove()
describe 'constructor', ->
describe 'always', ->
beforeEach ->
@speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
it 'add the video speed control to player', ->
secondaryControls = $('.secondary-controls')
li = secondaryControls.find('.video_speeds li')
expect(secondaryControls).toContain '.speeds'
expect(secondaryControls).toContain '.video_speeds'
expect(secondaryControls.find('p.active').text()).toBe '1.0x'
expect(li.filter('.active')).toHaveData 'speed', @speedControl.currentSpeed
expect(li.length).toBe @speedControl.speeds.length
$.each li.toArray().reverse(), (index, link) =>
expect($(link)).toHaveData 'speed', @speedControl.speeds[index]
expect($(link).find('a').text()).toBe @speedControl.speeds[index] + 'x'
it 'bind to change video speed link', ->
expect($('.video_speeds a')).toHandleWith 'click', @speedControl.changeVideoSpeed
describe 'when running on touch based device', ->
beforeEach ->
window.onTouchBasedDevice.andReturn true
$('.speeds').removeClass 'open'
@speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
it 'open the speed toggle on click', ->
$('.speeds').click()
expect($('.speeds')).toHaveClass 'open'
$('.speeds').click()
expect($('.speeds')).not.toHaveClass 'open'
describe 'when running on non-touch based device', ->
beforeEach ->
$('.speeds').removeClass 'open'
@speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
it 'open the speed toggle on hover', ->
$('.speeds').mouseenter()
expect($('.speeds')).toHaveClass 'open'
$('.speeds').mouseleave()
expect($('.speeds')).not.toHaveClass 'open'
it 'close the speed toggle on mouse out', ->
$('.speeds').mouseenter().mouseleave()
expect($('.speeds')).not.toHaveClass 'open'
it 'close the speed toggle on click', ->
$('.speeds').mouseenter().click()
expect($('.speeds')).not.toHaveClass 'open'
describe 'changeVideoSpeed', ->
beforeEach ->
@speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
@video.setSpeed '1.0'
describe 'when new speed is the same', ->
beforeEach ->
spyOnEvent @speedControl, 'speedChange'
$('li[data-speed="1.0"] a').click()
it 'does not trigger speedChange event', ->
expect('speedChange').not.toHaveBeenTriggeredOn @speedControl
describe 'when new speed is not the same', ->
beforeEach ->
@newSpeed = null
$(@speedControl).bind 'speedChange', (event, newSpeed) => @newSpeed = newSpeed
spyOnEvent @speedControl, 'speedChange'
$('li[data-speed="0.75"] a').click()
it 'trigger speedChange event', ->
expect('speedChange').toHaveBeenTriggeredOn @speedControl
expect(@newSpeed).toEqual 0.75
describe 'onSpeedChange', ->
beforeEach ->
@speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
$('li[data-speed="1.0"] a').addClass 'active'
@speedControl.setSpeed '0.75'
it 'set the new speed as active', ->
expect($('.video_speeds li[data-speed="1.0"]')).not.toHaveClass 'active'
expect($('.video_speeds li[data-speed="0.75"]')).toHaveClass 'active'
expect($('.speeds p.active')).toHaveHtml '0.75x'
describe 'VideoVolumeControlAlpha', ->
beforeEach ->
jasmine.stubVideoPlayerAlpha @
$('.volume').remove()
describe 'constructor', ->
beforeEach ->
spyOn($.fn, 'slider')
@volumeControl = new VideoVolumeControlAlpha el: $('.secondary-controls')
it 'initialize currentVolume to 100', ->
expect(@volumeControl.currentVolume).toEqual 100
it 'render the volume control', ->
expect($('.secondary-controls').html()).toContain """
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
"""
it 'create the slider', ->
expect($.fn.slider).toHaveBeenCalledWith
orientation: "vertical"
range: "min"
min: 0
max: 100
value: 100
change: @volumeControl.onChange
slide: @volumeControl.onChange
it 'bind the volume control', ->
expect($('.volume>a')).toHandleWith 'click', @volumeControl.toggleMute
expect($('.volume')).not.toHaveClass 'open'
$('.volume').mouseenter()
expect($('.volume')).toHaveClass 'open'
$('.volume').mouseleave()
expect($('.volume')).not.toHaveClass 'open'
describe 'onChange', ->
beforeEach ->
spyOnEvent @volumeControl, 'volumeChange'
@newVolume = undefined
@volumeControl = new VideoVolumeControlAlpha el: $('.secondary-controls')
$(@volumeControl).bind 'volumeChange', (event, volume) => @newVolume = volume
describe 'when the new volume is more than 0', ->
beforeEach ->
@volumeControl.onChange undefined, value: 60
it 'set the player volume', ->
expect(@newVolume).toEqual 60
it 'remote muted class', ->
expect($('.volume')).not.toHaveClass 'muted'
describe 'when the new volume is 0', ->
beforeEach ->
@volumeControl.onChange undefined, value: 0
it 'set the player volume', ->
expect(@newVolume).toEqual 0
it 'add muted class', ->
expect($('.volume')).toHaveClass 'muted'
describe 'toggleMute', ->
beforeEach ->
@newVolume = undefined
@volumeControl = new VideoVolumeControlAlpha el: $('.secondary-controls')
$(@volumeControl).bind 'volumeChange', (event, volume) => @newVolume = volume
describe 'when the current volume is more than 0', ->
beforeEach ->
@volumeControl.currentVolume = 60
@volumeControl.toggleMute()
it 'save the previous volume', ->
expect(@volumeControl.previousVolume).toEqual 60
it 'set the player volume', ->
expect(@newVolume).toEqual 0
describe 'when the current volume is 0', ->
beforeEach ->
@volumeControl.currentVolume = 0
@volumeControl.previousVolume = 60
@volumeControl.toggleMute()
it 'set the player volume to previous volume', ->
expect(@newVolume).toEqual 60
describe 'VideoAlpha', ->
metadata =
slowerSpeedYoutubeId:
id: @slowerSpeedYoutubeId
duration: 300
normalSpeedYoutubeId:
id: @normalSpeedYoutubeId
duration: 200
beforeEach ->
jasmine.stubRequests()
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
@videosDefinition = '0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
@slowerSpeedYoutubeId = 'slowerSpeedYoutubeId'
@normalSpeedYoutubeId = 'normalSpeedYoutubeId'
afterEach ->
window.OldVideoPlayerAlpha = undefined
window.onYouTubePlayerAPIReady = undefined
window.onHTML5PlayerAPIReady = undefined
describe 'constructor', ->
describe 'YT', ->
beforeEach ->
loadFixtures 'videoalpha.html'
@stubVideoPlayerAlpha = jasmine.createSpy('VideoPlayerAlpha')
$.cookie.andReturn '0.75'
describe 'by default', ->
beforeEach ->
spyOn(window.VideoAlpha.prototype, 'fetchMetadata').andCallFake ->
@metadata = metadata
@video = new VideoAlpha '#example', @videosDefinition
it 'check videoType', ->
expect(@video.videoType).toEqual('youtube')
it 'reset the current video player', ->
expect(window.OldVideoPlayerAlpha).toBeUndefined()
it 'set the elements', ->
expect(@video.el).toBe '#video_id'
it 'parse the videos', ->
expect(@video.videos).toEqual
'0.75': @slowerSpeedYoutubeId
'1.0': @normalSpeedYoutubeId
it 'fetch the video metadata', ->
expect(@video.fetchMetadata).toHaveBeenCalled
expect(@video.metadata).toEqual metadata
it 'parse available video speeds', ->
expect(@video.speeds).toEqual ['0.75', '1.0']
it 'set current video speed via cookie', ->
expect(@video.speed).toEqual '0.75'
it 'store a reference for this video player in the element', ->
expect($('.video').data('video')).toEqual @video
describe 'when the Youtube API is already available', ->
beforeEach ->
@originalYT = window.YT
window.YT = { Player: true }
spyOn(window, 'VideoPlayerAlpha').andReturn(@stubVideoPlayerAlpha)
@video = new VideoAlpha '#example', @videosDefinition
afterEach ->
window.YT = @originalYT
it 'create the Video Player', ->
expect(window.VideoPlayerAlpha).toHaveBeenCalledWith(video: @video)
expect(@video.player).toEqual @stubVideoPlayerAlpha
describe 'when the Youtube API is not ready', ->
beforeEach ->
@originalYT = window.YT
window.YT = {}
@video = new VideoAlpha '#example', @videosDefinition
afterEach ->
window.YT = @originalYT
it 'set the callback on the window object', ->
expect(window.onYouTubePlayerAPIReady).toEqual jasmine.any(Function)
describe 'when the Youtube API becoming ready', ->
beforeEach ->
@originalYT = window.YT
window.YT = {}
spyOn(window, 'VideoPlayerAlpha').andReturn(@stubVideoPlayerAlpha)
@video = new VideoAlpha '#example', @videosDefinition
window.onYouTubePlayerAPIReady()
afterEach ->
window.YT = @originalYT
it 'create the Video Player for all video elements', ->
expect(window.VideoPlayerAlpha).toHaveBeenCalledWith(video: @video)
expect(@video.player).toEqual @stubVideoPlayerAlpha
describe 'HTML5', ->
beforeEach ->
loadFixtures 'videoalpha_html5.html'
@stubVideoPlayerAlpha = jasmine.createSpy('VideoPlayerAlpha')
$.cookie.andReturn '0.75'
describe 'by default', ->
beforeEach ->
@originalHTML5 = window.HTML5Video.Player
window.HTML5Video.Player = undefined
@video = new VideoAlpha '#example', @videosDefinition
afterEach ->
window.HTML5Video.Player = @originalHTML5
it 'check videoType', ->
expect(@video.videoType).toEqual('html5')
it 'reset the current video player', ->
expect(window.OldVideoPlayerAlpha).toBeUndefined()
it 'set the elements', ->
expect(@video.el).toBe '#video_id'
it 'parse the videos if subtitles exist', ->
sub = 'test_name_of_the_subtitles'
expect(@video.videos).toEqual
'0.75': sub
'1.0': sub
'1.25': sub
'1.5': sub
it 'parse the videos if subtitles doesn\'t exist', ->
$('#example').find('.video').data('sub', '')
@video = new VideoAlpha '#example', @videosDefinition
sub = ''
expect(@video.videos).toEqual
'0.75': sub
'1.0': sub
'1.25': sub
'1.5': sub
it 'parse Html5 sources', ->
html5Sources =
mp4: 'test.mp4'
webm: 'test.webm'
ogg: 'test.ogv'
expect(@video.html5Sources).toEqual html5Sources
it 'parse available video speeds', ->
speeds = jasmine.stubbedHtml5Speeds
expect(@video.speeds).toEqual speeds
it 'set current video speed via cookie', ->
expect(@video.speed).toEqual '0.75'
it 'store a reference for this video player in the element', ->
expect($('.video').data('video')).toEqual @video
describe 'when the HTML5 API is already available', ->
beforeEach ->
@originalHTML5Video = window.HTML5Video
window.HTML5Video = { Player: true }
spyOn(window, 'VideoPlayerAlpha').andReturn(@stubVideoPlayerAlpha)
@video = new VideoAlpha '#example', @videosDefinition
afterEach ->
window.HTML5Video = @originalHTML5Video
it 'create the Video Player', ->
expect(window.VideoPlayerAlpha).toHaveBeenCalledWith(video: @video)
expect(@video.player).toEqual @stubVideoPlayerAlpha
describe 'when the HTML5 API is not ready', ->
beforeEach ->
@originalHTML5Video = window.HTML5Video
window.HTML5Video = {}
@video = new VideoAlpha '#example', @videosDefinition
afterEach ->
window.HTML5Video = @originalHTML5Video
it 'set the callback on the window object', ->
expect(window.onHTML5PlayerAPIReady).toEqual jasmine.any(Function)
describe 'when the HTML5 API becoming ready', ->
beforeEach ->
@originalHTML5Video = window.HTML5Video
window.HTML5Video = {}
spyOn(window, 'VideoPlayerAlpha').andReturn(@stubVideoPlayerAlpha)
@video = new VideoAlpha '#example', @videosDefinition
window.onHTML5PlayerAPIReady()
afterEach ->
window.HTML5Video = @originalHTML5Video
it 'create the Video Player for all video elements', ->
expect(window.VideoPlayerAlpha).toHaveBeenCalledWith(video: @video)
expect(@video.player).toEqual @stubVideoPlayerAlpha
describe 'youtubeId', ->
beforeEach ->
loadFixtures 'videoalpha.html'
$.cookie.andReturn '1.0'
@video = new VideoAlpha '#example', @videosDefinition
describe 'with speed', ->
it 'return the video id for given speed', ->
expect(@video.youtubeId('0.75')).toEqual @slowerSpeedYoutubeId
expect(@video.youtubeId('1.0')).toEqual @normalSpeedYoutubeId
describe 'without speed', ->
it 'return the video id for current speed', ->
expect(@video.youtubeId()).toEqual @normalSpeedYoutubeId
describe 'setSpeed', ->
describe 'YT', ->
beforeEach ->
loadFixtures 'videoalpha.html'
@video = new VideoAlpha '#example', @videosDefinition
describe 'when new speed is available', ->
beforeEach ->
@video.setSpeed '0.75'
it 'set new speed', ->
expect(@video.speed).toEqual '0.75'
it 'save setting for new speed', ->
expect($.cookie).toHaveBeenCalledWith 'video_speed', '0.75', expires: 3650, path: '/'
describe 'when new speed is not available', ->
beforeEach ->
@video.setSpeed '1.75'
it 'set speed to 1.0x', ->
expect(@video.speed).toEqual '1.0'
describe 'HTML5', ->
beforeEach ->
loadFixtures 'videoalpha_html5.html'
@video = new VideoAlpha '#example', @videosDefinition
describe 'when new speed is available', ->
beforeEach ->
@video.setSpeed '0.75'
it 'set new speed', ->
expect(@video.speed).toEqual '0.75'
it 'save setting for new speed', ->
expect($.cookie).toHaveBeenCalledWith 'video_speed', '0.75', expires: 3650, path: '/'
describe 'when new speed is not available', ->
beforeEach ->
@video.setSpeed '1.75'
it 'set speed to 1.0x', ->
expect(@video.speed).toEqual '1.0'
describe 'getDuration', ->
beforeEach ->
loadFixtures 'videoalpha.html'
@video = new VideoAlpha '#example', @videosDefinition
it 'return duration for current video', ->
expect(@video.getDuration()).toEqual 200
describe 'log', ->
beforeEach ->
loadFixtures 'videoalpha.html'
@video = new VideoAlpha '#example', @videosDefinition
spyOn Logger, 'log'
@video.log 'someEvent', {
currentTime: 25,
speed: '1.0'
}
it 'call the logger with valid extra parameters', ->
expect(Logger.log).toHaveBeenCalledWith 'someEvent',
id: 'id'
code: @normalSpeedYoutubeId
currentTime: 25
speed: '1.0'
Jasmine JavaScript tests status
-------------------------------
As of 22.07.2013, all the tests in this directory pass. To disable each of them, change the top level "describe(" to "xdescribe(".
PS: When you are running the tests in chrome locally, make sure that chrome is started
with the option "--allow-file-access-from-files".
(function() {
xdescribe('VideoControlAlpha', function() {
var state, videoControl, oldOTBD;
function initialize() {
loadFixtures('videoalpha_all.html');
state = new VideoAlpha('#example');
videoControl = state.videoControl;
}
beforeEach(function(){
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
});
afterEach(function() {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
});
describe('constructor', function() {
beforeEach(function() {
initialize();
});
it('render the video controls', function() {
expect($('.video-controls')).toContain(
['.slider', 'ul.vcr', 'a.play', '.vidtime', '.add-fullscreen'].join(',')
);
expect($('.video-controls').find('.vidtime')).toHaveText('0:00 / 0:00');
});
it('bind the playback button', function() {
expect($('.video_control')).toHandleWith('click', videoControl.togglePlayback);
});
describe('when on a non-touch based device', function() {
beforeEach(function() {
initialize();
});
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(true);
initialize();
});
it('does not add the play class to video control', function() {
expect($('.video_control')).not.toHaveClass('play');
expect($('.video_control')).not.toHaveAttr('title', 'Play');
});
});
});
describe('play', function() {
beforeEach(function() {
initialize();
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() {
initialize();
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() {
initialize();
});
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(videoControl, 'pause');
videoControl.togglePlayback(jQuery.Event('click'));
});
it('does not trigger the pause event', function() {
expect('pause').not.toHaveBeenTriggeredOn(videoControl);
});
});
describe('when the video is paused', function() {
beforeEach(function() {
$('.video_control').addClass('pause');
spyOnEvent(videoControl, 'play');
videoControl.togglePlayback(jQuery.Event('click'));
});
it('does not trigger the play event', function() {
expect('play').not.toHaveBeenTriggeredOn(videoControl);
});
});
});
});
});
}).call(this);
(function() {
xdescribe('VideoProgressSliderAlpha', function() {
var state, videoPlayer, videoProgressSlider, oldOTBD;
function initialize() {
loadFixtures('videoalpha_all.html');
state = new VideoAlpha('#example');
videoPlayer = state.videoPlayer;
videoProgressSlider = state.videoProgressSlider;
}
beforeEach(function() {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
});
afterEach(function() {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
});
describe('constructor', function() {
describe('on a non-touch based device', function() {
beforeEach(function() {
spyOn($.fn, 'slider').andCallThrough();
initialize();
});
it('build the slider', function() {
expect(videoProgressSlider.slider).toBe('.slider');
expect($.fn.slider).toHaveBeenCalledWith({
range: 'min',
change: videoProgressSlider.onChange,
slide: videoProgressSlider.onSlide,
stop: videoProgressSlider.onStop
});
});
it('build the seek handle', function() {
expect(videoProgressSlider.handle).toBe('.slider .ui-slider-handle');
expect($.fn.qtip).toHaveBeenCalledWith({
content: "0:00",
position: {
my: 'bottom center',
at: 'top center',
container: videoProgressSlider.handle
},
hide: {
delay: 700
},
style: {
classes: 'ui-tooltip-slider',
widget: true
}
});
});
});
describe('on a touch-based device', function() {
beforeEach(function() {
window.onTouchBasedDevice.andReturn(true);
spyOn($.fn, 'slider').andCallThrough();
initialize();
});
it('does not build the slider', function() {
expect(videoProgressSlider.slider).toBeUndefined();
// We can't expect $.fn.slider not to have been called,
// because sliders are used in other parts of VideoAlpha.
// expect($.fn.slider).not.toHaveBeenCalled();
});
});
});
describe('play', function() {
beforeEach(function() {
initialize();
});
describe('when the slider was already built', function() {
var spy;
beforeEach(function() {
spy = spyOn(videoProgressSlider, 'buildSlider');
spy.andCallThrough();
videoPlayer.play();
});
it('does not build the slider', function() {
expect(spy.callCount).toEqual(0);
});
});
// Currently, the slider is not rebuilt if it does not exist.
//
// describe('when the slider was not already built', function() {
// beforeEach(function() {
// spyOn($.fn, 'slider').andCallThrough();
// videoProgressSlider.slider = null;
// videoPlayer.play();
// });
//
// it('build the slider', function() {
// expect(videoProgressSlider.slider).toBe('.slider');
// expect($.fn.slider).toHaveBeenCalledWith({
// range: 'min',
// change: videoProgressSlider.onChange,
// slide: videoProgressSlider.onSlide,
// stop: videoProgressSlider.onStop
// });
// });
//
// it('build the seek handle', function() {
// expect(videoProgressSlider.handle).toBe('.ui-slider-handle');
// expect($.fn.qtip).toHaveBeenCalledWith({
// content: "0:00",
// position: {
// my: 'bottom center',
// at: 'top center',
// container: videoProgressSlider.handle
// },
// hide: {
// delay: 700
// },
// style: {
// classes: 'ui-tooltip-slider',
// widget: true
// }
// });
// });
// });
});
describe('updatePlayTime', function() {
beforeEach(function() {
initialize();
});
describe('when frozen', function() {
beforeEach(function() {
spyOn($.fn, 'slider').andCallThrough();
videoProgressSlider.frozen = true;
videoProgressSlider.updatePlayTime(20, 120);
});
it('does not update the slider', function() {
expect($.fn.slider).not.toHaveBeenCalled();
});
});
describe('when not frozen', function() {
beforeEach(function() {
spyOn($.fn, 'slider').andCallThrough();
videoProgressSlider.frozen = false;
videoProgressSlider.updatePlayTime({time:20, duration:120});
});
it('update the max value of the slider', function() {
expect($.fn.slider).toHaveBeenCalledWith('option', 'max', 120);
});
it('update current value of the slider', function() {
expect($.fn.slider).toHaveBeenCalledWith('option', 'value', 20);
});
});
});
describe('onSlide', function() {
beforeEach(function() {
initialize();
spyOn($.fn, 'slider').andCallThrough();
spyOn(videoPlayer, 'onSlideSeek').andCallThrough();
videoProgressSlider.onSlide({}, {
value: 20
});
});
it('freeze the slider', function() {
expect(videoProgressSlider.frozen).toBeTruthy();
});
it('update the tooltip', function() {
expect($.fn.qtip).toHaveBeenCalled();
});
it('trigger seek event', function() {
expect(videoPlayer.onSlideSeek).toHaveBeenCalled();
expect(videoPlayer.currentTime).toEqual(20);
});
});
describe('onChange', function() {
beforeEach(function() {
initialize();
spyOn($.fn, 'slider').andCallThrough();
videoProgressSlider.onChange({}, {
value: 20
});
});
it('update the tooltip', function() {
expect($.fn.qtip).toHaveBeenCalled();
});
});
describe('onStop', function() {
beforeEach(function() {
initialize();
spyOn(videoPlayer, 'onSlideSeek').andCallThrough();
videoProgressSlider.onStop({}, {
value: 20
});
});
it('freeze the slider', function() {
expect(videoProgressSlider.frozen).toBeTruthy();
});
it('trigger seek event', function() {
expect(videoPlayer.onSlideSeek).toHaveBeenCalled();
expect(videoPlayer.currentTime).toEqual(20);
});
it('set timeout to unfreeze the slider', function() {
expect(window.setTimeout).toHaveBeenCalledWith(jasmine.any(Function), 200);
window.setTimeout.mostRecentCall.args[0]();
expect(videoProgressSlider.frozen).toBeFalsy();
});
});
describe('updateTooltip', function() {
beforeEach(function() {
initialize();
spyOn($.fn, 'slider').andCallThrough();
videoProgressSlider.updateTooltip(90);
});
it('set the tooltip value', function() {
expect($.fn.qtip).toHaveBeenCalledWith('option', 'content.text', '1:30');
});
});
});
}).call(this);
(function() {
xdescribe('VideoQualityControlAlpha', function() {
var state, videoControl, videoQualityControl, oldOTBD;
function initialize() {
loadFixtures('videoalpha.html');
state = new VideoAlpha('#example');
videoControl = state.videoControl;
videoQualityControl = state.videoQualityControl;
}
beforeEach(function() {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
});
afterEach(function() {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
});
describe('constructor', function() {
beforeEach(function() {
initialize();
});
it('render the quality control', function() {
expect(videoControl.secondaryControlsEl.html()).toContain("<a href=\"#\" class=\"quality_control\" title=\"HD\">");
});
it('bind the quality control', function() {
expect($('.quality_control')).toHandleWith('click', videoQualityControl.toggleQuality);
});
});
});
}).call(this);
(function() {
xdescribe('VideoSpeedControlAlpha', function() {
var state, videoPlayer, videoControl, videoSpeedControl;
function initialize() {
loadFixtures('videoalpha_all.html');
state = new VideoAlpha('#example');
videoPlayer = state.videoPlayer;
videoControl = state.videoControl;
videoSpeedControl = state.videoSpeedControl;
}
beforeEach(function() {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
});
afterEach(function() {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
});
describe('constructor', function() {
describe('always', function() {
beforeEach(function() {
initialize();
});
it('add the video speed control to player', function() {
var li, secondaryControls;
secondaryControls = $('.secondary-controls');
li = secondaryControls.find('.video_speeds li');
expect(secondaryControls).toContain('.speeds');
expect(secondaryControls).toContain('.video_speeds');
expect(secondaryControls.find('p.active').text()).toBe('1.0x');
expect(li.filter('.active')).toHaveData('speed', videoSpeedControl.currentSpeed);
expect(li.length).toBe(videoSpeedControl.speeds.length);
$.each(li.toArray().reverse(), function(index, link) {
expect($(link)).toHaveData('speed', videoSpeedControl.speeds[index]);
expect($(link).find('a').text()).toBe(videoSpeedControl.speeds[index] + 'x');
});
});
it('bind to change video speed link', function() {
expect($('.video_speeds a')).toHandleWith('click', videoSpeedControl.changeVideoSpeed);
});
});
describe('when running on touch based device', function() {
beforeEach(function() {
window.onTouchBasedDevice.andReturn(true);
initialize();
});
it('open the speed toggle on click', function() {
$('.speeds').click();
expect($('.speeds')).toHaveClass('open');
$('.speeds').click();
expect($('.speeds')).not.toHaveClass('open');
});
});
describe('when running on non-touch based device', function() {
beforeEach(function() {
initialize();
});
it('open the speed toggle on hover', function() {
$('.speeds').mouseenter();
expect($('.speeds')).toHaveClass('open');
$('.speeds').mouseleave();
expect($('.speeds')).not.toHaveClass('open');
});
it('close the speed toggle on mouse out', function() {
$('.speeds').mouseenter().mouseleave();
expect($('.speeds')).not.toHaveClass('open');
});
it('close the speed toggle on click', function() {
$('.speeds').mouseenter().click();
expect($('.speeds')).not.toHaveClass('open');
});
});
});
describe('changeVideoSpeed', function() {
// This is an unnecessary test. The internal browser API, and YouTube API
// detect (and do not do anything) if there is a request for a speed that
// is already set.
//
// describe('when new speed is the same', function() {
// beforeEach(function() {
// initialize();
// videoSpeedControl.setSpeed(1.0);
// spyOn(videoPlayer, 'onSpeedChange').andCallThrough();
//
// $('li[data-speed="1.0"] a').click();
// });
//
// it('does not trigger speedChange event', function() {
// expect(videoPlayer.onSpeedChange).not.toHaveBeenCalled();
// });
// });
describe('when new speed is not the same', function() {
beforeEach(function() {
initialize();
videoSpeedControl.setSpeed(1.0);
spyOn(videoPlayer, 'onSpeedChange').andCallThrough();
$('li[data-speed="0.75"] a').click();
});
it('trigger speedChange event', function() {
expect(videoPlayer.onSpeedChange).toHaveBeenCalled();
expect(videoSpeedControl.currentSpeed).toEqual(0.75);
});
});
});
describe('onSpeedChange', function() {
beforeEach(function() {
initialize();
$('li[data-speed="1.0"] a').addClass('active');
videoSpeedControl.setSpeed(0.75);
});
it('set the new speed as active', function() {
expect($('.video_speeds li[data-speed="1.0"]')).not.toHaveClass('active');
expect($('.video_speeds li[data-speed="0.75"]')).toHaveClass('active');
expect($('.speeds p.active')).toHaveHtml('0.75x');
});
});
});
}).call(this);
(function() {
xdescribe('VideoVolumeControlAlpha', function() {
var state, videoControl, videoVolumeControl, oldOTBD;
function initialize() {
loadFixtures('videoalpha_all.html');
state = new VideoAlpha('#example');
videoControl = state.videoControl;
videoVolumeControl = state.videoVolumeControl;
}
beforeEach(function() {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
});
afterEach(function() {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
});
describe('constructor', function() {
beforeEach(function() {
spyOn($.fn, 'slider').andCallThrough();
initialize();
});
it('initialize currentVolume to 100', function() {
expect(state.videoVolumeControl.currentVolume).toEqual(1);
});
it('render the volume control', function() {
expect(videoControl.secondaryControlsEl.html()).toContain("<div class=\"volume\">\n");
});
it('create the slider', function() {
expect($.fn.slider).toHaveBeenCalledWith({
orientation: "vertical",
range: "min",
min: 0,
max: 100,
/* value: 100, */
value: videoVolumeControl.currentVolume,
change: videoVolumeControl.onChange,
slide: videoVolumeControl.onChange
});
});
it('bind the volume control', function() {
expect($('.volume>a')).toHandleWith('click', videoVolumeControl.toggleMute);
expect($('.volume')).not.toHaveClass('open');
$('.volume').mouseenter();
expect($('.volume')).toHaveClass('open');
$('.volume').mouseleave();
expect($('.volume')).not.toHaveClass('open');
});
});
describe('onChange', function() {
beforeEach(function() {
initialize();
});
describe('when the new volume is more than 0', function() {
beforeEach(function() {
videoVolumeControl.onChange(void 0, {
value: 60
});
});
it('set the player volume', function() {
expect(videoVolumeControl.currentVolume).toEqual(60);
});
it('remote muted class', function() {
expect($('.volume')).not.toHaveClass('muted');
});
});
describe('when the new volume is 0', function() {
beforeEach(function() {
videoVolumeControl.onChange(void 0, {
value: 0
});
});
it('set the player volume', function() {
expect(videoVolumeControl.currentVolume).toEqual(0);
});
it('add muted class', function() {
expect($('.volume')).toHaveClass('muted');
});
});
});
describe('toggleMute', function() {
beforeEach(function() {
initialize();
});
describe('when the current volume is more than 0', function() {
beforeEach(function() {
videoVolumeControl.currentVolume = 60;
videoVolumeControl.buttonEl.trigger('click');
});
it('save the previous volume', function() {
expect(videoVolumeControl.previousVolume).toEqual(60);
});
it('set the player volume', function() {
expect(videoVolumeControl.currentVolume).toEqual(0);
});
});
describe('when the current volume is 0', function() {
beforeEach(function() {
videoVolumeControl.currentVolume = 0;
videoVolumeControl.previousVolume = 60;
videoVolumeControl.buttonEl.trigger('click');
});
it('set the player volume to previous volume', function() {
expect(videoVolumeControl.currentVolume).toEqual(60);
});
});
});
});
}).call(this);
......@@ -2,3 +2,7 @@
# For each of the xmodules subdirectories, add a .gitignore file that
# will version any *.js file that is specifically written, not compiled.
*.js
# Videoalpha are written in pure JavaScript.
!videoalpha/*.js
\ No newline at end of file
......@@ -87,6 +87,9 @@ class @Sequence
modx_full_url = @modx_url + '/' + @id + '/goto_position'
$.postWithPrefix modx_full_url, position: new_position
# On Sequence change, fire custom event "sequence:change" on element.
# Added for aborting video bufferization, see ../videoalpha/10_main.js
@el.trigger "sequence:change"
@mark_active new_position
@$('#seq_content').html @contents.eq(new_position - 1).text()
XModule.loadModules(@$('#seq_content'))
......@@ -140,7 +143,7 @@ class @Sequence
analytics.track "Accessed Next Sequential",
sequence_id: @id
current_sequential: @position
target_sequential: new_position
target_sequential: new_position
@render new_position
......
(function (requirejs, require, define) {
// VideoControl module.
define(
'videoalpha/04_video_control.js',
[],
function () {
// VideoControl() function - what this module "exports".
return function (state) {
state.videoControl = {};
_makeFunctionsPublic(state);
_renderElements(state);
_bindHandlers(state);
};
// ***************************************************************
// 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) {
state.videoControl.showControls = _.bind(showControls,state);
state.videoControl.hideControls = _.bind(hideControls,state);
state.videoControl.play = _.bind(play,state);
state.videoControl.pause = _.bind(pause,state);
state.videoControl.togglePlayback = _.bind(togglePlayback,state);
state.videoControl.toggleFullScreen = _.bind(toggleFullScreen,state);
state.videoControl.exitFullScreen = _.bind(exitFullScreen,state);
state.videoControl.updateVcrVidTime = _.bind(updateVcrVidTime,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) {
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.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;
if (!onTouchBasedDevice()) {
state.videoControl.pause();
state.videoControl.playPauseEl.qtip(state.config.qTipConfig);
state.videoControl.fullScreenEl.qtip(state.config.qTipConfig);
} else {
state.videoControl.play();
}
if (state.videoType === 'html5') {
state.videoControl.fadeOutTimeout = state.config.fadeOutTimeout;
state.videoControl.el.addClass('html5');
state.controlHideTimeout = setTimeout(state.videoControl.hideControls, state.videoControl.fadeOutTimeout);
}
}
// 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.toggleFullScreen);
$(document).on('keyup', state.videoControl.exitFullScreen);
if (state.videoType === 'html5') {
state.el.on('mousemove', state.videoControl.showControls)
}
}
// ***************************************************************
// 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 showControls(event) {
if (!this.controlShowLock) {
if (!this.captionsHidden) {
return;
}
this.controlShowLock = true;
if (this.controlState === 'invisible') {
this.videoControl.el.show();
this.controlState = 'visible';
} else if (this.controlState === 'hiding') {
this.videoControl.el.stop(true, false).css('opacity', 1).show();
this.controlState = 'visible';
} else if (this.controlState === 'visible') {
clearTimeout(this.controlHideTimeout);
}
this.controlHideTimeout = setTimeout(this.videoControl.hideControls, this.videoControl.fadeOutTimeout);
this.controlShowLock = false;
}
}
function hideControls() {
var _this;
this.controlHideTimeout = null;
if (!this.captionsHidden) {
return;
}
this.controlState = 'hiding';
_this = this;
this.videoControl.el.fadeOut(this.videoControl.fadeOutTimeout, function () {
_this.controlState = 'invisible';
});
}
function play() {
this.videoControl.playPauseEl.removeClass('play').addClass('pause').attr('title', gettext('Pause'));
this.videoControl.isPlaying = true;
}
function pause() {
this.videoControl.playPauseEl.removeClass('pause').addClass('play').attr('title', gettext('Play'));
this.videoControl.isPlaying = false;
}
function togglePlayback(event) {
event.preventDefault();
if (this.videoControl.isPlaying) {
this.trigger('videoPlayer.pause', null);
} else {
this.trigger('videoPlayer.play', null);
}
}
function toggleFullScreen(event) {
event.preventDefault();
var fullScreenClassNameEl = this.el.add(document.documentElement);
if (this.videoControl.fullScreenState) {
this.videoControl.fullScreenState = false;
fullScreenClassNameEl.removeClass('video-fullscreen');
this.isFullScreen = false;
this.videoControl.fullScreenEl.attr('title', gettext('Fullscreen'));
} else {
this.videoControl.fullScreenState = true;
fullScreenClassNameEl.addClass('video-fullscreen');
this.isFullScreen = true;
this.videoControl.fullScreenEl.attr('title', gettext('Exit fullscreen'));
}
this.trigger('videoCaption.resize', null);
}
function exitFullScreen(event) {
if ((this.isFullScreen) && (event.keyCode === 27)) {
this.videoControl.toggleFullScreen(event);
}
}
function updateVcrVidTime(params) {
this.videoControl.vidTimeEl.html(Time.format(params.time) + ' / ' + Time.format(params.duration));
}
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
(function (requirejs, require, define) {
// VideoQualityControl module.
define(
'videoalpha/05_video_quality_control.js',
[],
function () {
// VideoQualityControl() function - what this module "exports".
return function (state) {
// Changing quality for now only works for YouTube videos.
if (state.videoType !== 'youtube') {
return;
}
state.videoQualityControl = {};
_makeFunctionsPublic(state);
_renderElements(state);
_bindHandlers(state);
};
// ***************************************************************
// 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) {
state.videoQualityControl.onQualityChange = _.bind(onQualityChange, state);
state.videoQualityControl.toggleQuality = _.bind(toggleQuality, 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) {
state.videoQualityControl.el = state.el.find('a.quality_control');
state.videoQualityControl.el.show();
state.videoQualityControl.quality = null;
if (!onTouchBasedDevice()) {
state.videoQualityControl.el.qtip(state.config.qTipConfig);
}
}
// function _bindHandlers(state)
//
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
function _bindHandlers(state) {
state.videoQualityControl.el.on('click', state.videoQualityControl.toggleQuality);
}
// ***************************************************************
// 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 onQualityChange(value) {
this.videoQualityControl.quality = value;
if (_.indexOf(this.config.availableQualities, value) !== -1) {
this.videoQualityControl.el.addClass('active');
} else {
this.videoQualityControl.el.removeClass('active');
}
}
// This function change quality of video.
// Right now we haven't ability to choose quality of HD video,
// 'hd720' will be played by default as HD video(this thing is hardcoded).
// If suggested quality level is not available for the video,
// then the quality will be set to the next lowest level that is available.
// (large -> medium)
function toggleQuality(event) {
var newQuality,
value = this.videoQualityControl.quality;
event.preventDefault();
if (_.indexOf(this.config.availableQualities, value) !== -1) {
newQuality = 'large';
} else {
newQuality = 'hd720';
}
this.trigger('videoPlayer.handlePlaybackQualityChange', newQuality);
}
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
(function (requirejs, require, define) {
/*
"This is as true in everyday life as it is in battle: we are given one life
and the decision is ours whether to wait for circumstances to make up our
mind, or whether to act, and in acting, to live."
— Omar N. Bradley
*/
// VideoProgressSlider module.
define(
'videoalpha/06_video_progress_slider.js',
[],
function () {
// VideoProgressSlider() function - what this module "exports".
return function (state) {
state.videoProgressSlider = {};
_makeFunctionsPublic(state);
_renderElements(state);
// No callbacks to DOM events (click, mousemove, etc.).
};
// ***************************************************************
// 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) {
state.videoProgressSlider.onSlide = _.bind(onSlide, state);
state.videoProgressSlider.onChange = _.bind(onChange, state);
state.videoProgressSlider.onStop = _.bind(onStop, state);
state.videoProgressSlider.updateTooltip = _.bind(updateTooltip, state);
state.videoProgressSlider.updatePlayTime = _.bind(updatePlayTime, state);
//Added for tests -- JM
state.videoProgressSlider.buildSlider = _.bind(buildSlider, 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) {
if (!onTouchBasedDevice()) {
state.videoProgressSlider.el = state.videoControl.sliderEl;
buildSlider(state);
_buildHandle(state);
}
}
function _buildHandle(state) {
state.videoProgressSlider.handle = state.videoProgressSlider.el.find('.ui-slider-handle');
state.videoProgressSlider.handle.qtip({
content: '' + Time.format(state.videoProgressSlider.slider.slider('value')),
position: {
my: 'bottom center',
at: 'top center',
container: state.videoProgressSlider.handle
},
hide: {
delay: 700
},
style: {
classes: 'ui-tooltip-slider',
widget: true
}
});
}
// ***************************************************************
// 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 buildSlider(state) {
state.videoProgressSlider.slider = state.videoProgressSlider.el.slider({
range: 'min',
change: state.videoProgressSlider.onChange,
slide: state.videoProgressSlider.onSlide,
stop: state.videoProgressSlider.onStop
});
}
function onSlide(event, ui) {
this.videoProgressSlider.frozen = true;
this.videoProgressSlider.updateTooltip(ui.value);
this.trigger('videoPlayer.onSlideSeek', {'type': 'onSlideSeek', 'time': ui.value});
}
function onChange(event, ui) {
this.videoProgressSlider.updateTooltip(ui.value);
}
function onStop(event, ui) {
var _this = this;
this.videoProgressSlider.frozen = true;
this.trigger('videoPlayer.onSlideSeek', {'type': 'onSlideSeek', 'time': ui.value});
setTimeout(function() {
_this.videoProgressSlider.frozen = false;
}, 200);
}
function updateTooltip(value) {
this.videoProgressSlider.handle.qtip('option', 'content.text', '' + Time.format(value));
}
//Changed for tests -- JM: Check if it is the cause of Chrome Bug Valera noticed
function updatePlayTime(params) {
if ((this.videoProgressSlider.slider) && (!this.videoProgressSlider.frozen)) {
/*this.videoProgressSlider.slider
.slider('option', 'max', params.duration)
.slider('value', params.time);*/
this.videoProgressSlider.slider.slider('option', 'max', params.duration);
this.videoProgressSlider.slider.slider('option', 'value', params.time);
}
}
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
(function (requirejs, require, define) {
// VideoVolumeControl module.
define(
'videoalpha/07_video_volume_control.js',
[],
function () {
// VideoVolumeControl() function - what this module "exports".
return function (state) {
state.videoVolumeControl = {};
_makeFunctionsPublic(state);
_renderElements(state);
_bindHandlers(state);
};
// ***************************************************************
// 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) {
state.videoVolumeControl.onChange = _.bind(onChange, state);
state.videoVolumeControl.toggleMute = _.bind(toggleMute, 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) {
state.videoVolumeControl.el = state.el.find('div.volume');
state.videoVolumeControl.buttonEl = state.videoVolumeControl.el.find('a');
state.videoVolumeControl.volumeSliderEl = state.videoVolumeControl.el.find('.volume-slider');
state.videoControl.secondaryControlsEl.prepend(state.videoVolumeControl.el);
// Figure out what the current volume is. If no information about volume level could be retrieved,
// then we will use the default 100 level (full volume).
state.videoVolumeControl.currentVolume = parseInt($.cookie('video_player_volume_level'), 10);
if (!isFinite(state.videoVolumeControl.currentVolume)) {
state.videoVolumeControl.currentVolume = 100;
}
// Set it up so that muting/unmuting works correctly.
state.videoVolumeControl.previousVolume = 100;
state.videoVolumeControl.slider = state.videoVolumeControl.volumeSliderEl.slider({
orientation: 'vertical',
range: 'min',
min: 0,
max: 100,
value: state.videoVolumeControl.currentVolume,
change: state.videoVolumeControl.onChange,
slide: state.videoVolumeControl.onChange
});
state.videoVolumeControl.el.toggleClass('muted', state.videoVolumeControl.currentVolume === 0);
}
// function _bindHandlers(state)
//
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
function _bindHandlers(state) {
state.videoVolumeControl.buttonEl.on('click', state.videoVolumeControl.toggleMute);
state.videoVolumeControl.el.on('mouseenter', function() {
$(this).addClass('open');
});
state.videoVolumeControl.el.on('mouseleave', function() {
$(this).removeClass('open');
});
}
// ***************************************************************
// 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 onChange(event, ui) {
this.videoVolumeControl.currentVolume = ui.value;
this.videoVolumeControl.el.toggleClass('muted', this.videoVolumeControl.currentVolume === 0);
$.cookie('video_player_volume_level', ui.value, {
expires: 3650,
path: '/'
});
this.trigger('videoPlayer.onVolumeChange', ui.value);
}
function toggleMute(event) {
event.preventDefault();
if (this.videoVolumeControl.currentVolume > 0) {
this.videoVolumeControl.previousVolume = this.videoVolumeControl.currentVolume;
this.videoVolumeControl.slider.slider('option', 'value', 0);
} else {
this.videoVolumeControl.slider.slider('option', 'value', this.videoVolumeControl.previousVolume);
}
}
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
(function (requirejs, require, define) {
// VideoSpeedControl module.
define(
'videoalpha/08_video_speed_control.js',
[],
function () {
// VideoSpeedControl() function - what this module "exports".
return function (state) {
state.videoSpeedControl = {};
_makeFunctionsPublic(state);
_renderElements(state);
_bindHandlers(state);
};
// ***************************************************************
// 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) {
state.videoSpeedControl.changeVideoSpeed = _.bind(changeVideoSpeed, state);
state.videoSpeedControl.setSpeed = _.bind(setSpeed, state);
state.videoSpeedControl.reRender = _.bind(reRender, 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) {
state.videoSpeedControl.speeds = state.speeds;
state.videoSpeedControl.el = state.el.find('div.speeds');
state.videoSpeedControl.videoSpeedsEl = state.videoSpeedControl.el.find('.video_speeds');
state.videoControl.secondaryControlsEl.prepend(state.videoSpeedControl.el);
$.each(state.videoSpeedControl.speeds, function(index, speed) {
//var link = $('<a href="#">' + speed + 'x</a>');
var link = '<a href="#">' + speed + 'x</a>';
state.videoSpeedControl.videoSpeedsEl.prepend($('<li data-speed="' + speed + '">' + link + '</li>'));
});
state.videoSpeedControl.setSpeed(state.speed);
}
// function _bindHandlers(state)
//
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
function _bindHandlers(state) {
state.videoSpeedControl.videoSpeedsEl.find('a').on('click', state.videoSpeedControl.changeVideoSpeed);
if (onTouchBasedDevice()) {
state.videoSpeedControl.el.on('click', function(event) {
event.preventDefault();
$(this).toggleClass('open');
});
} else {
state.videoSpeedControl.el
.on('mouseenter', function () {
$(this).addClass('open');
})
.on('mouseleave', function () {
$(this).removeClass('open');
})
.on('click', function (event) {
event.preventDefault();
$(this).removeClass('open');
});
}
}
// ***************************************************************
// 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 setSpeed(speed) {
this.videoSpeedControl.videoSpeedsEl.find('li').removeClass('active');
this.videoSpeedControl.videoSpeedsEl.find("li[data-speed='" + speed + "']").addClass('active');
this.videoSpeedControl.el.find('p.active').html('' + speed + 'x');
}
function changeVideoSpeed(event) {
var parentEl = $(event.target).parent();
event.preventDefault();
if (!parentEl.hasClass('active')) {
this.videoSpeedControl.currentSpeed = parentEl.data('speed');
this.videoSpeedControl.setSpeed(
// To meet the API expected format.
parseFloat(this.videoSpeedControl.currentSpeed).toFixed(2).replace(/\.00$/, '.0')
);
this.trigger('videoPlayer.onSpeedChange', this.videoSpeedControl.currentSpeed);
}
}
function reRender(params) {
var _this = this;
this.videoSpeedControl.videoSpeedsEl.empty();
this.videoSpeedControl.videoSpeedsEl.find('li').removeClass('active');
this.videoSpeedControl.speeds = params.newSpeeds;
$.each(this.videoSpeedControl.speeds, function(index, speed) {
var link, listItem;
//link = $('<a href="#">' + speed + 'x</a>');
link = '<a href="#">' + speed + 'x</a>';
listItem = $('<li data-speed="' + speed + '">' + link + '</li>');
if (speed === params.currentSpeed) {
listItem.addClass('active');
}
_this.videoSpeedControl.videoSpeedsEl.prepend(listItem);
});
this.videoSpeedControl.videoSpeedsEl.find('a').on('click', this.videoSpeedControl.changeVideoSpeed);
}
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
(function (requirejs, require, define) {
// Main module.
require(
[
'videoalpha/01_initialize.js',
'videoalpha/04_video_control.js',
'videoalpha/05_video_quality_control.js',
'videoalpha/06_video_progress_slider.js',
'videoalpha/07_video_volume_control.js',
'videoalpha/08_video_speed_control.js',
'videoalpha/09_video_caption.js'
],
function (
Initialize,
VideoControl,
VideoQualityControl,
VideoProgressSlider,
VideoVolumeControl,
VideoSpeedControl,
VideoCaption
) {
var previousState;
// 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.
previousState = null;
window.VideoAlpha = function (element) {
var state;
// Stop bufferization of previous video on sequence change.
// Problem: multiple video tags with the same src cannot
// play together. The second tag waiting when first video will be fully loaded.
// That's why we abort bufferization forcibly.
$(element).closest('.sequence').bind('sequence:change', function(e){
if (previousState !== null && typeof previousState.videoPlayer !== 'undefined') {
previousState.stopBuffering();
$(e.currentTarget).unbind('sequence:change');
}
});
// 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 !== null && typeof previousState.videoPlayer !== 'undefined') {
previousState.videoPlayer.onPause();
}
state = {};
previousState = state;
Initialize(state, element);
VideoControl(state);
VideoQualityControl(state);
VideoProgressSlider(state);
VideoVolumeControl(state);
VideoSpeedControl(state);
if (state.config.show_captions) {
VideoCaption(state);
}
// 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
// VideoAlpha with Jasmine.
return state;
};
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
class @VideoAlpha
constructor: (element) ->
@el = $(element).find('.video')
@id = @el.attr('id').replace(/video_/, '')
@start = @el.data('start')
@end = @el.data('end')
@caption_data_dir = @el.data('caption-data-dir')
@caption_asset_path = @el.data('caption-asset-path')
@show_captions = @el.data('show-captions').toString() == "true"
@el = $("#video_#{@id}")
if @parseYoutubeId(@el.data("streams")) is true
@videoType = "youtube"
@fetchMetadata()
@parseSpeed()
else
@videoType = "html5"
@parseHtml5Sources @el.data('mp4-source'), @el.data('webm-source'), @el.data('ogg-source')
@speeds = ['0.75', '1.0', '1.25', '1.50']
sub = @el.data('sub')
if (typeof sub isnt "string") or (sub.length is 0)
sub = ""
@show_captions = false
@videos =
"0.75": sub
"1.0": sub
"1.25": sub
"1.5": sub
@setSpeed $.cookie('video_speed')
$("#video_#{@id}").data('video', this).addClass('video-load-complete')
if @show_captions is true
@hide_captions = $.cookie('hide_captions') == 'true'
else
@hide_captions = true
$.cookie('hide_captions', @hide_captions, expires: 3650, path: '/')
@el.addClass 'closed'
if ((@videoType is "youtube") and (YT.Player)) or ((@videoType is "html5") and (HTML5Video.Player))
@embed()
else
if @videoType is "youtube"
window.onYouTubePlayerAPIReady = =>
@embed()
else if @videoType is "html5"
window.onHTML5PlayerAPIReady = =>
@embed()
youtubeId: (speed)->
@videos[speed || @speed]
parseYoutubeId: (videos)->
return false if (typeof videos isnt "string") or (videos.length is 0)
@videos = {}
$.each videos.split(/,/), (index, video) =>
speed = undefined
video = video.split(/:/)
speed = parseFloat(video[0]).toFixed(2).replace(/\.00$/, ".0")
@videos[speed] = video[1]
true
parseHtml5Sources: (mp4Source, webmSource, oggSource)->
@html5Sources =
mp4: null
webm: null
ogg: null
@html5Sources.mp4 = mp4Source if (typeof mp4Source is "string") and (mp4Source.length > 0)
@html5Sources.webm = webmSource if (typeof webmSource is "string") and (webmSource.length > 0)
@html5Sources.ogg = oggSource if (typeof oggSource is "string") and (oggSource.length > 0)
parseSpeed: ->
@speeds = ($.map @videos, (url, speed) -> speed).sort()
@setSpeed $.cookie('video_speed')
setSpeed: (newSpeed, updateCookie)->
if @speeds.indexOf(newSpeed) isnt -1
@speed = newSpeed
if updateCookie isnt false
$.cookie "video_speed", "" + newSpeed,
expires: 3650
path: "/"
else
@speed = "1.0"
embed: ->
@player = new VideoPlayerAlpha video: this
fetchMetadata: (url) ->
@metadata = {}
$.each @videos, (speed, url) =>
$.get "https://gdata.youtube.com/feeds/api/videos/#{url}?v=2&alt=jsonc", ((data) => @metadata[data.data.id] = data.data) , 'jsonp'
getDuration: ->
@metadata[@youtubeId()].duration
log: (eventName, data)->
# Default parameters that always get logged.
logInfo =
id: @id
code: @youtubeId()
# If extra parameters were passed to the log.
if data
$.each data, (paramName, value) ->
logInfo[paramName] = value
if @videoType is "youtube"
logInfo.code = @youtubeId()
else logInfo.code = "html5" if @videoType is "html5"
Logger.log eventName, logInfo
class @SubviewAlpha
constructor: (options) ->
$.each options, (key, value) =>
@[key] = value
@initialize()
@render()
@bind()
$: (selector) ->
$(selector, @el)
initialize: ->
render: ->
bind: ->
class @VideoCaptionAlpha extends SubviewAlpha
initialize: ->
@loaded = false
bind: ->
$(window).bind('resize', @resize)
@$('.hide-subtitles').click @toggle
@$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave)
.mousemove(@onMovement).bind('mousewheel', @onMovement)
.bind('DOMMouseScroll', @onMovement)
captionURL: ->
"#{@captionAssetPath}#{@youtubeId}.srt.sjson"
render: ->
# TODO: make it so you can have a video with no captions.
#@$('.video-wrapper').after """
# <ol class="subtitles"><li>Attempting to load captions...</li></ol>
# """
@$('.video-wrapper').after """
<ol class="subtitles"></ol>
"""
@$('.video-controls .secondary-controls').append """
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
"""#"
@$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5
@fetchCaption()
fetchCaption: ->
$.ajaxWithPrefix
url: @captionURL()
notifyOnError: false
success: (captions) =>
@captions = captions.text
@start = captions.start
@loaded = true
if onTouchBasedDevice()
$('.subtitles').html "<li>Caption will be displayed when you start playing the video.</li>"
else
@renderCaption()
renderCaption: ->
container = $('<ol>')
$.each @captions, (index, text) =>
container.append $('<li>').html(text).attr
'data-index': index
'data-start': @start[index]
@$('.subtitles').html(container.html())
@$('.subtitles li[data-index]').click @seekPlayer
# prepend and append an empty <li> for cosmetic reason
@$('.subtitles').prepend($('<li class="spacing">').height(@topSpacingHeight()))
.append($('<li class="spacing">').height(@bottomSpacingHeight()))
@rendered = true
search: (time) ->
if @loaded
min = 0
max = @start.length - 1
while min < max
index = Math.ceil((max + min) / 2)
if time < @start[index]
max = index - 1
if time >= @start[index]
min = index
return min
play: ->
if @loaded
@renderCaption() unless @rendered
@playing = true
pause: ->
if @loaded
@playing = false
updatePlayTime: (time) ->
if @loaded
# This 250ms offset is required to match the video speed
time = Math.round(Time.convert(time, @currentSpeed, '1.0') * 1000 + 250)
newIndex = @search time
if newIndex != undefined && @currentIndex != newIndex
if @currentIndex
@$(".subtitles li.current").removeClass('current')
@$(".subtitles li[data-index='#{newIndex}']").addClass('current')
@currentIndex = newIndex
@scrollCaption()
resize: =>
@$('.subtitles').css maxHeight: @captionHeight()
@$('.subtitles .spacing:first').height(@topSpacingHeight())
@$('.subtitles .spacing:last').height(@bottomSpacingHeight())
@scrollCaption()
onMouseEnter: =>
clearTimeout @frozen if @frozen
@frozen = setTimeout @onMouseLeave, 10000
onMovement: =>
@onMouseEnter()
onMouseLeave: =>
clearTimeout @frozen if @frozen
@frozen = null
@scrollCaption() if @playing
scrollCaption: ->
if !@frozen && @$('.subtitles .current:first').length
@$('.subtitles').scrollTo @$('.subtitles .current:first'),
offset: - @calculateOffset(@$('.subtitles .current:first'))
seekPlayer: (event) =>
event.preventDefault()
time = Math.round(Time.convert($(event.target).data('start'), '1.0', @currentSpeed) / 1000)
$(@).trigger('caption_seek', time)
calculateOffset: (element) ->
@captionHeight() / 2 - element.height() / 2
topSpacingHeight: ->
@calculateOffset(@$('.subtitles li:not(.spacing):first'))
bottomSpacingHeight: ->
@calculateOffset(@$('.subtitles li:not(.spacing):last'))
toggle: (event) =>
event.preventDefault()
if @el.hasClass('closed') # Captions are "closed" e.g. turned off
@hideCaptions(false)
else # Captions are on
@hideCaptions(true)
hideCaptions: (hide_captions) =>
if hide_captions
type = 'hide_transcript'
@$('.hide-subtitles').attr('title', 'Turn on captions')
@el.addClass('closed')
else
type = 'show_transcript'
@$('.hide-subtitles').attr('title', 'Turn off captions')
@el.removeClass('closed')
@scrollCaption()
@video.log type,
currentTime: @player.currentTime
$.cookie('hide_captions', hide_captions, expires: 3650, path: '/')
captionHeight: ->
if @el.hasClass('fullscreen')
$(window).height() - @$('.video-controls').height()
else
@$('.video-wrapper').height()
class @VideoControlAlpha extends SubviewAlpha
bind: ->
@$('.video_control').click @togglePlayback
render: ->
@el.append """
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#"></a></li>
<li>
<div class="vidtime">0:00 / 0:00</div>
</li>
</ul>
<div class="secondary-controls">
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
</div>
</div>
"""#"
unless onTouchBasedDevice()
@$('.video_control').addClass('play').html('Play')
play: ->
@$('.video_control').removeClass('play').addClass('pause').html('Pause')
pause: ->
@$('.video_control').removeClass('pause').addClass('play').html('Play')
togglePlayback: (event) =>
event.preventDefault()
if @$('.video_control').hasClass('play')
$(@).trigger('play')
else if @$('.video_control').hasClass('pause')
$(@).trigger('pause')
class @VideoProgressSliderAlpha extends SubviewAlpha
initialize: ->
@buildSlider() unless onTouchBasedDevice()
buildSlider: ->
@slider = @el.slider
range: 'min'
change: @onChange
slide: @onSlide
stop: @onStop
@buildHandle()
buildHandle: ->
@handle = @$('.ui-slider-handle')
@handle.qtip
content: "#{Time.format(@slider.slider('value'))}"
position:
my: 'bottom center'
at: 'top center'
container: @handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
play: =>
@buildSlider() unless @slider
updatePlayTime: (currentTime, duration) ->
if @slider && !@frozen
@slider.slider('option', 'max', duration)
@slider.slider('value', currentTime)
onSlide: (event, ui) =>
@frozen = true
@updateTooltip(ui.value)
$(@).trigger('slide_seek', ui.value)
onChange: (event, ui) =>
@updateTooltip(ui.value)
onStop: (event, ui) =>
@frozen = true
$(@).trigger('slide_seek', ui.value)
setTimeout (=> @frozen = false), 200
updateTooltip: (value)->
@handle.qtip('option', 'content.text', "#{Time.format(value)}")
class @VideoQualityControlAlpha extends SubviewAlpha
initialize: ->
@quality = null;
bind: ->
@$('.quality_control').click @toggleQuality
render: ->
@el.append """
<a href="#" class="quality_control" title="HD">HD</a>
"""#"
onQualityChange: (value) ->
@quality = value
if @quality in ['hd720', 'hd1080', 'highres']
@el.addClass('active')
else
@el.removeClass('active')
toggleQuality: (event) =>
event.preventDefault()
if @quality in ['hd720', 'hd1080', 'highres']
newQuality = 'large'
else
newQuality = 'hd720'
$(@).trigger('changeQuality', newQuality)
\ No newline at end of file
class @VideoSpeedControlAlpha extends SubviewAlpha
bind: ->
@$('.video_speeds a').click @changeVideoSpeed
if onTouchBasedDevice()
@$('.speeds').click (event) ->
event.preventDefault()
$(this).toggleClass('open')
else
@$('.speeds').mouseenter ->
$(this).addClass('open')
@$('.speeds').mouseleave ->
$(this).removeClass('open')
@$('.speeds').click (event) ->
event.preventDefault()
$(this).removeClass('open')
render: ->
@el.prepend """
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
</a>
<ol class="video_speeds"></ol>
</div>
"""
$.each @speeds, (index, speed) =>
link = $('<a>').attr(href: "#").html("#{speed}x")
@$('.video_speeds').prepend($('<li>').attr('data-speed', speed).html(link))
@setSpeed @currentSpeed
reRender: (newSpeeds, currentSpeed) ->
@$('.video_speeds').empty()
@$('.video_speeds li').removeClass('active')
@speeds = newSpeeds
$.each @speeds, (index, speed) =>
link = $('<a>').attr(href: "#").html("#{speed}x")
listItem = $('<li>').attr('data-speed', speed).html(link);
listItem.addClass('active') if speed is currentSpeed
@$('.video_speeds').prepend listItem
@$('.video_speeds a').click @changeVideoSpeed
changeVideoSpeed: (event) =>
event.preventDefault()
unless $(event.target).parent().hasClass('active')
@currentSpeed = $(event.target).parent().data('speed')
$(@).trigger 'speedChange', $(event.target).parent().data('speed')
@setSpeed(parseFloat(@currentSpeed).toFixed(2).replace /\.00$/, '.0')
setSpeed: (speed) ->
@$('.video_speeds li').removeClass('active')
@$(".video_speeds li[data-speed='#{speed}']").addClass('active')
@$('.speeds p.active').html("#{speed}x")
class @VideoVolumeControlAlpha extends SubviewAlpha
initialize: ->
@currentVolume = 100
bind: ->
@$('.volume').mouseenter ->
$(this).addClass('open')
@$('.volume').mouseleave ->
$(this).removeClass('open')
@$('.volume>a').click(@toggleMute)
render: ->
@el.prepend """
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
"""#"
@slider = @$('.volume-slider').slider
orientation: "vertical"
range: "min"
min: 0
max: 100
value: 100
change: @onChange
slide: @onChange
onChange: (event, ui) =>
@currentVolume = ui.value
$(@).trigger 'volumeChange', @currentVolume
@$('.volume').toggleClass 'muted', @currentVolume == 0
toggleMute: =>
if @currentVolume > 0
@previousVolume = @currentVolume
@slider.slider 'option', 'value', 0
else
@slider.slider 'option', 'value', @previousVolume
......@@ -14,11 +14,14 @@ import fs.osfs
import numpy
import json
import calc
import xmodule
from xmodule.x_module import ModuleSystem
from mock import Mock
open_ended_grading_interface = {
'url': 'blah/',
'username': 'incorrect_user',
......@@ -111,3 +114,36 @@ class ModelsTest(unittest.TestCase):
except:
exception_happened = True
self.assertTrue(exception_happened)
class PostData(object):
"""Class which emulate postdata."""
def __init__(self, dict_data):
self.dict_data = dict_data
def getlist(self, key):
"""Get data by key from `self.dict_data`."""
return self.dict_data.get(key)
class LogicTest(unittest.TestCase):
"""Base class for testing xmodule logic."""
descriptor_class = None
raw_model_data = {}
def setUp(self):
class EmptyClass:
"""Empty object."""
url_name = ''
category = 'test'
self.system = get_test_system()
self.descriptor = EmptyClass()
self.xmodule_class = self.descriptor_class.module_class
self.xmodule = self.xmodule_class(
self.system, self.descriptor, self.raw_model_data)
def ajax_request(self, dispatch, data):
"""Call Xmodule.handle_ajax."""
return json.loads(self.xmodule.handle_ajax(dispatch, data))
# -*- coding: utf-8 -*-
"""Test for Conditional Xmodule functional logic."""
from xmodule.conditional_module import ConditionalDescriptor
from . import LogicTest
class ConditionalModuleTest(LogicTest):
"""Logic tests for Conditional Xmodule."""
descriptor_class = ConditionalDescriptor
def test_ajax_request(self):
"Make shure that ajax request works correctly"
# Mock is_condition_satisfied
self.xmodule.is_condition_satisfied = lambda: True
setattr(self.xmodule.descriptor, 'get_children', lambda: [])
response = self.ajax_request('No', {})
html = response['html']
self.assertEqual(html, [])
# -*- coding: utf-8 -*-
"""Test for Poll Xmodule functional logic."""
from xmodule.poll_module import PollDescriptor
from . import LogicTest
class PollModuleTest(LogicTest):
"""Logic tests for Poll Xmodule."""
descriptor_class = PollDescriptor
raw_model_data = {
'poll_answers': {'Yes': 1, 'Dont_know': 0, 'No': 0},
'voted': False,
'poll_answer': ''
}
def test_bad_ajax_request(self):
# Make sure that answer for incorrect request is error json.
response = self.ajax_request('bad_answer', {})
self.assertDictEqual(response, {'error': 'Unknown Command!'})
def test_good_ajax_request(self):
# Make sure that ajax request works correctly.
response = self.ajax_request('No', {})
poll_answers = response['poll_answers']
total = response['total']
callback = response['callback']
self.assertDictEqual(poll_answers, {'Yes': 1, 'Dont_know': 0, 'No': 1})
self.assertEqual(total, 2)
self.assertDictEqual(callback, {'objectName': 'Conditional'})
self.assertEqual(self.xmodule.poll_answer, 'No')
......@@ -13,15 +13,12 @@ common/lib/xmodule/xmodule/modulestore/tests/factories.py to create the
course, section, subsection, unit, etc.
"""
import json
import unittest
from mock import Mock
from lxml import etree
from xmodule.video_module import VideoDescriptor, VideoModule, _parse_time, _parse_youtube
from xmodule.modulestore import Location
from xmodule.tests import get_test_system
from xmodule.tests.test_logic import LogicTest
from xmodule.tests import LogicTest
class VideoFactory(object):
......
# -*- coding: utf-8 -*-
"""Test for Video Alpha Xmodule functional logic.
These tests data readed from xml, not from mongo.
we have a ModuleStoreTestCase class defined in
common/lib/xmodule/xmodule/modulestore/tests/django_utils.py. You can
search for usages of this in the cms and lms tests for examples. You use
this so that it will do things like point the modulestore setting to mongo,
flush the contentstore before and after, load the templates, etc.
You can then use the CourseFactory and XModuleItemFactory as defined
in common/lib/xmodule/xmodule/modulestore/tests/factories.py to create
the course, section, subsection, unit, etc.
"""
from xmodule.videoalpha_module import VideoAlphaDescriptor
from . import LogicTest
from lxml import etree
class VideoAlphaModuleTest(LogicTest):
"""Logic tests for VideoAlpha Xmodule."""
descriptor_class = VideoAlphaDescriptor
raw_model_data = {
'data': '<videoalpha />'
}
def test_get_timeframe_no_parameters(self):
"Make sure that timeframe() works correctly w/o parameters"
xmltree = etree.fromstring('<videoalpha>test</videoalpha>')
output = self.xmodule.get_timeframe(xmltree)
self.assertEqual(output, ('', ''))
def test_get_timeframe_with_one_parameter(self):
"Make sure that timeframe() works correctly with one parameter"
xmltree = etree.fromstring(
'<videoalpha start_time="00:04:07">test</videoalpha>'
)
output = self.xmodule.get_timeframe(xmltree)
self.assertEqual(output, (247, ''))
def test_get_timeframe_with_two_parameters(self):
"Make sure that timeframe() works correctly with two parameters"
xmltree = etree.fromstring(
'''<videoalpha
start_time="00:04:07"
end_time="13:04:39"
>test</videoalpha>'''
)
output = self.xmodule.get_timeframe(xmltree)
self.assertEqual(output, (247, 47079))
# -*- coding: utf-8 -*-
# pylint: disable=W0232
"""Test for Xmodule functional logic."""
"""Test for Word cloud Xmodule functional logic."""
import json
import unittest
from xmodule.poll_module import PollDescriptor
from xmodule.conditional_module import ConditionalDescriptor
from xmodule.word_cloud_module import WordCloudDescriptor
from xmodule.tests import get_test_system
class PostData:
"""Class which emulate postdata."""
def __init__(self, dict_data):
self.dict_data = dict_data
def getlist(self, key):
"""Get data by key from `self.dict_data`."""
return self.dict_data.get(key)
class LogicTest(unittest.TestCase):
"""Base class for testing xmodule logic."""
descriptor_class = None
raw_model_data = {}
def setUp(self):
class EmptyClass:
"""Empty object."""
url_name = ''
category = 'test'
self.system = get_test_system()
self.descriptor = EmptyClass()
self.xmodule_class = self.descriptor_class.module_class
self.xmodule = self.xmodule_class(
self.system,
self.descriptor,
self.raw_model_data
)
def ajax_request(self, dispatch, data):
"""Call Xmodule.handle_ajax."""
return json.loads(self.xmodule.handle_ajax(dispatch, data))
class PollModuleTest(LogicTest):
"""Logic tests for Poll Xmodule."""
descriptor_class = PollDescriptor
raw_model_data = {
'poll_answers': {'Yes': 1, 'Dont_know': 0, 'No': 0},
'voted': False,
'poll_answer': ''
}
def test_bad_ajax_request(self):
response = self.ajax_request('bad_answer', {})
self.assertDictEqual(response, {'error': 'Unknown Command!'})
def test_good_ajax_request(self):
response = self.ajax_request('No', {})
poll_answers = response['poll_answers']
total = response['total']
callback = response['callback']
self.assertDictEqual(poll_answers, {'Yes': 1, 'Dont_know': 0, 'No': 1})
self.assertEqual(total, 2)
self.assertDictEqual(callback, {'objectName': 'Conditional'})
self.assertEqual(self.xmodule.poll_answer, 'No')
class ConditionalModuleTest(LogicTest):
"""Logic tests for Conditional Xmodule."""
descriptor_class = ConditionalDescriptor
def test_ajax_request(self):
# Mock is_condition_satisfied
self.xmodule.is_condition_satisfied = lambda: True
setattr(self.xmodule.descriptor, 'get_children', lambda: [])
response = self.ajax_request('No', {})
html = response['html']
self.assertEqual(html, [])
from . import PostData, LogicTest
class WordCloudModuleTest(LogicTest):
......@@ -97,6 +15,7 @@ class WordCloudModuleTest(LogicTest):
}
def test_bad_ajax_request(self):
"Make sure that answer for incorrect request is error json"
response = self.ajax_request('bad_dispatch', {})
self.assertDictEqual(response, {
'status': 'fail',
......@@ -104,6 +23,7 @@ class WordCloudModuleTest(LogicTest):
})
def test_good_ajax_request(self):
"Make shure that ajax request works correctly"
post_data = PostData({'student_words[]': ['cat', 'cat', 'dog', 'sun']})
response = self.ajax_request('submit', post_data)
self.assertEqual(response['status'], 'success')
......@@ -125,3 +45,4 @@ class WordCloudModuleTest(LogicTest):
self.assertEqual(
100.0,
sum(i['percent'] for i in response['top_words']))
......@@ -68,14 +68,19 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
icon_class = 'video'
js = {
'js': [resource_string(__name__, 'js/src/videoalpha/display/html5_video.js')],
'coffee':
[resource_string(__name__, 'js/src/time.coffee'),
resource_string(__name__, 'js/src/videoalpha/display.coffee')] +
[resource_string(__name__, 'js/src/videoalpha/display/' + filename)
for filename
in sorted(resource_listdir(__name__, 'js/src/videoalpha/display'))
if filename.endswith('.coffee')]}
'js': [
resource_string(__name__, 'js/src/videoalpha/01_initialize.js'),
resource_string(__name__, 'js/src/videoalpha/02_html5_video.js'),
resource_string(__name__, 'js/src/videoalpha/03_video_player.js'),
resource_string(__name__, 'js/src/videoalpha/04_video_control.js'),
resource_string(__name__, 'js/src/videoalpha/05_video_quality_control.js'),
resource_string(__name__, 'js/src/videoalpha/06_video_progress_slider.js'),
resource_string(__name__, 'js/src/videoalpha/07_video_volume_control.js'),
resource_string(__name__, 'js/src/videoalpha/08_video_speed_control.js'),
resource_string(__name__, 'js/src/videoalpha/09_video_caption.js'),
resource_string(__name__, 'js/src/videoalpha/10_main.js')
]
}
css = {'scss': [resource_string(__name__, 'css/videoalpha/display.scss')]}
js_module_name = "VideoAlpha"
......@@ -87,6 +92,11 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
self.youtube_streams = xmltree.get('youtube', '')
self.sub = xmltree.get('sub')
self.autoplay = xmltree.get('autoplay') or ''
if self.autoplay.lower() not in ['true', 'false']:
self.autoplay = 'true'
self.position = 0
self.show_captions = xmltree.get('show_captions', 'true')
self.sources = {
......@@ -162,6 +172,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
'youtube_streams': self.youtube_streams,
'id': self.location.html_id(),
'sub': self.sub,
'autoplay': self.autoplay,
'sources': self.sources,
'track': self.track,
'display_name': self.display_name_with_default,
......
......@@ -315,8 +315,8 @@ jasmine.JQuery.matchersClass = {};
|| jasmine.isDomNode(this.actual))) {
this.actual = $(this.actual)
var result = jQueryMatchers[methodName].apply(this, arguments)
var element;
if (this.actual.get && (element = this.actual.get()[0]) && !$.isWindow(element) && element.tagName !== "HTML")
var element;
if (this.actual.get && (element = this.actual.get()[0]) && !$.isWindow(element) && element.tagName !== "HTML")
this.actual = jasmine.JQuery.elementToString(this.actual)
return result
}
......
......@@ -7,4 +7,4 @@ Feature: Video component
Scenario: Autoplay is enabled in the LMS for a VideoAlpha component
Given the course has a VideoAlpha component
Then when I view the video it has autoplay enabled
Then when I view the videoalpha it has autoplay enabled
......@@ -8,10 +8,15 @@ from common import i_am_registered_for_the_course, section_location
@step('when I view the video it has autoplay enabled')
def does_autoplay(_step):
def does_autoplay_video(_step):
assert(world.css_find('.video')[0]['data-autoplay'] == 'True')
@step('when I view the videoalpha it has autoplay enabled')
def does_autoplay_videoalpha(_step):
assert(world.css_find('.videoalpha')[0]['data-autoplay'] == 'True')
@step('the course has a Video component')
def view_video(_step):
coursenum = 'test_course'
......
......@@ -23,7 +23,7 @@ from django.conf import settings
from xmodule.videoalpha_module import VideoAlphaDescriptor, VideoAlphaModule
from xmodule.modulestore import Location
from xmodule.tests import get_test_system
from xmodule.tests.test_logic import LogicTest
from xmodule.tests import LogicTest
SOURCE_XML = """
......
......@@ -3,6 +3,14 @@ html {
max-height: 100%;
}
html.video-fullscreen{
overflow: hidden;
body{
overflow: hidden;
}
}
div.course-wrapper {
section.course-content {
......
......@@ -6,14 +6,14 @@
<div
id="video_${id}"
class="video"
class="videoalpha"
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
data-streams="${youtube_streams}"
% endif
${'data-sub="{}"'.format(sub) if sub else ''}
${'data-autoplay="{}"'.format(autoplay) if autoplay else ''}
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''}
${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''}
......@@ -29,11 +29,47 @@
>
<div class="tc-wrapper">
<article class="video-wrapper">
<div class="video-player-pre"></div>
<section class="video-player">
<div id="${id}"></div>
</section>
<section class="video-controls"></section>
<div class="video-player-post"></div>
<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 href="#">
<h3>${_('Speed')}</h3>
<p class="active"></p>
</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" title="${_('HD')}">${_('HD')}</a>
% if show_captions == 'true':
<a href="#" class="hide-subtitles" title="${_('Turn off captions')}">${_('Captions')}</a>
% endif
</div>
</div>
</section>
</article>
% if show_captions == 'true':
<ol class="subtitles"><li></li></ol>
% endif
</div>
</div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment