Merge pull request #1998 from edx/anton/fix-video-in-ipad
Fix video controls on iPad.
This commit is contained in:
@@ -5,6 +5,14 @@ These are notable changes in edx-platform. This is a rolling list of changes,
|
||||
in roughly chronological order, most recent first. Add your entries at or near
|
||||
the top. Include a label indicating the component affected.
|
||||
|
||||
Blades: Video player improvements:
|
||||
- Disable edX controls on iPhone/iPod (native controls are used).
|
||||
- Disable unsupported controls (volume, playback rate) on iPad/Android.
|
||||
- Controls becomes visible after click on video or play placeholder to avoid
|
||||
issues with YouTube API on iPad/Android.
|
||||
- Captions becomes visible just after full initialization of video player.
|
||||
- Fix blinking of captions after initialization of video player. BLD-206.
|
||||
|
||||
LMS: Fix answer distribution download for small courses. LMS-922, LMS-811
|
||||
|
||||
Blades: Add template for the zooming image in studio. BLD-206.
|
||||
|
||||
@@ -141,12 +141,13 @@ def the_youtube_video_is_shown(_step):
|
||||
@step('Make sure captions are (.+)$')
|
||||
def set_captions_visibility_state(_step, captions_state):
|
||||
SELECTOR = '.closed .subtitles'
|
||||
world.wait_for_visible('.hide-subtitles')
|
||||
if captions_state == 'closed':
|
||||
if not world.is_css_present(SELECTOR):
|
||||
world.browser.find_by_css('.hide-subtitles').click()
|
||||
world.css_find('.hide-subtitles').click()
|
||||
else:
|
||||
if world.is_css_present(SELECTOR):
|
||||
world.browser.find_by_css('.hide-subtitles').click()
|
||||
world.css_find('.hide-subtitles').click()
|
||||
|
||||
|
||||
@step('I hover over button "([^"]*)"$')
|
||||
|
||||
@@ -9,7 +9,7 @@ define ["domReady", "jquery", "underscore.string", "backbone", "gettext",
|
||||
window.CMS = window.CMS or {}
|
||||
CMS.URL = CMS.URL or {}
|
||||
window.onTouchBasedDevice = ->
|
||||
navigator.userAgent.match /iPhone|iPod|iPad/i
|
||||
navigator.userAgent.match /iPhone|iPod|iPad|Android/i
|
||||
|
||||
_.extend CMS, Backbone.Events
|
||||
Backbone.emulateHTTP = true
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.video {
|
||||
@include clearfix();
|
||||
background: #f3f3f3;
|
||||
@@ -97,12 +101,35 @@ div.video {
|
||||
}
|
||||
}
|
||||
|
||||
.btn-play {
|
||||
@include transform(translate(-50%, -50%));
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
padding: 30px;
|
||||
border-radius: 25%;
|
||||
|
||||
&:after{
|
||||
content: '';
|
||||
display: block;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
border-style: solid;
|
||||
border-width: 30px 0 30px 50px;
|
||||
border-color: transparent transparent transparent #ffffff;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
section.video-player {
|
||||
overflow: hidden;
|
||||
min-height: 300px;
|
||||
|
||||
div {
|
||||
> div {
|
||||
height: 100%;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -674,6 +701,7 @@ div.video {
|
||||
width: 275px;
|
||||
padding: 0 20px;
|
||||
z-index: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -764,6 +792,17 @@ div.video {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-touch {
|
||||
div.tc-wrapper {
|
||||
article.video-wrapper {
|
||||
object, iframe, video{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div id="example">
|
||||
<div
|
||||
id="video_id"
|
||||
class="video"
|
||||
class="video closed"
|
||||
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
|
||||
data-show-captions="true"
|
||||
data-start=""
|
||||
@@ -18,12 +18,14 @@
|
||||
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
|
||||
<span tabindex="-1" class="btn-play is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
|
||||
<div class="video-player-pre"></div>
|
||||
<section class="video-player">
|
||||
<div id="id"></div>
|
||||
</section>
|
||||
<div class="video-player-post"></div>
|
||||
<section class="video-controls">
|
||||
<section class="video-controls is-hidden">
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div id="example">
|
||||
<div
|
||||
id="video_id"
|
||||
class="video"
|
||||
class="video closed"
|
||||
data-show-captions="true"
|
||||
data-start=""
|
||||
data-end=""
|
||||
@@ -21,12 +21,14 @@
|
||||
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
|
||||
<span tabindex="-1" class="btn-play is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
|
||||
<div class="video-player-pre"></div>
|
||||
<section class="video-player">
|
||||
<div id="id"></div>
|
||||
</section>
|
||||
<div class="video-player-post"></div>
|
||||
<section class="video-controls">
|
||||
<section class="video-controls is-hidden">
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div id="example">
|
||||
<div
|
||||
id="video_id"
|
||||
class="video"
|
||||
class="video closed"
|
||||
data-show-captions="true"
|
||||
data-start=""
|
||||
data-end=""
|
||||
@@ -21,10 +21,12 @@
|
||||
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
|
||||
<span tabindex="-1" class="btn-play is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
|
||||
<section class="video-player">
|
||||
<div id="id"></div>
|
||||
</section>
|
||||
<section class="video-controls"></section>
|
||||
<section class="video-controls is-hidden"></section>
|
||||
</article>
|
||||
|
||||
<ol class="subtitles"><li></li></ol>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div id="example">
|
||||
<div
|
||||
id="video_id"
|
||||
class="video"
|
||||
class="video closed"
|
||||
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
|
||||
data-show-captions="false"
|
||||
data-start=""
|
||||
@@ -18,10 +18,12 @@
|
||||
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
|
||||
<span tabindex="-1" class="btn-play is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
|
||||
<section class="video-player">
|
||||
<div id="id"></div>
|
||||
</section>
|
||||
<section class="video-controls"></section>
|
||||
<section class="video-controls is-hidden"></section>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div id="example1">
|
||||
<div
|
||||
id="video_id1"
|
||||
class="video"
|
||||
class="video closed"
|
||||
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
|
||||
data-show-captions="true"
|
||||
data-start=""
|
||||
@@ -18,12 +18,14 @@
|
||||
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
|
||||
<span tabindex="-1" class="btn-play is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
|
||||
<div class="video-player-pre"></div>
|
||||
<section class="video-player">
|
||||
<div id="id1"></div>
|
||||
</section>
|
||||
<div class="video-player-post"></div>
|
||||
<section class="video-controls">
|
||||
<section class="video-controls is-hidden">
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
|
||||
164
common/lib/xmodule/xmodule/js/spec/video/events_spec.js
Normal file
164
common/lib/xmodule/xmodule/js/spec/video/events_spec.js
Normal file
@@ -0,0 +1,164 @@
|
||||
(function () {
|
||||
describe('VideoPlayer Events', function () {
|
||||
var state, videoPlayer, player, videoControl, videoCaption,
|
||||
videoProgressSlider, videoSpeedControl, videoVolumeControl,
|
||||
oldOTBD;
|
||||
|
||||
function initialize(fixture, params) {
|
||||
if (_.isString(fixture)) {
|
||||
loadFixtures(fixture);
|
||||
} else {
|
||||
if (_.isObject(fixture)) {
|
||||
params = fixture;
|
||||
}
|
||||
|
||||
loadFixtures('video_all.html');
|
||||
}
|
||||
|
||||
if (_.isObject(params)) {
|
||||
$('#example')
|
||||
.find('#video_id')
|
||||
.data(params);
|
||||
}
|
||||
|
||||
state = new Video('#example');
|
||||
|
||||
state.videoEl = $('video, iframe');
|
||||
videoPlayer = state.videoPlayer;
|
||||
player = videoPlayer.player;
|
||||
videoControl = state.videoControl;
|
||||
videoCaption = state.videoCaption;
|
||||
videoProgressSlider = state.videoProgressSlider;
|
||||
videoSpeedControl = state.videoSpeedControl;
|
||||
videoVolumeControl = state.videoVolumeControl;
|
||||
|
||||
state.resizer = (function () {
|
||||
var methods = [
|
||||
'align',
|
||||
'alignByWidthOnly',
|
||||
'alignByHeightOnly',
|
||||
'setParams',
|
||||
'setMode'
|
||||
],
|
||||
obj = {};
|
||||
|
||||
$.each(methods, function (index, method) {
|
||||
obj[method] = jasmine.createSpy(method).andReturn(obj);
|
||||
});
|
||||
|
||||
return obj;
|
||||
}());
|
||||
}
|
||||
|
||||
function initializeYouTube() {
|
||||
initialize('video.html');
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice')
|
||||
.andReturn(null);
|
||||
this.oldYT = window.YT;
|
||||
|
||||
jasmine.stubRequests();
|
||||
window.YT = {
|
||||
Player: function () {
|
||||
return {
|
||||
getPlaybackQuality: function () {}
|
||||
};
|
||||
},
|
||||
PlayerState: this.oldYT.PlayerState,
|
||||
ready: function (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
$('source').remove();
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
window.YT = this.oldYT;
|
||||
});
|
||||
|
||||
it('initialize', function(){
|
||||
runs(function () {
|
||||
initialize();
|
||||
});
|
||||
|
||||
waitsFor(function () {
|
||||
return state.el.hasClass('is-initialized');
|
||||
}, 'Player is not initialized.', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect('initialize').not.toHaveBeenTriggeredOn('.video');
|
||||
});
|
||||
});
|
||||
|
||||
it('ready', function() {
|
||||
runs(function () {
|
||||
initialize();
|
||||
});
|
||||
|
||||
waitsFor(function () {
|
||||
return state.el.hasClass('is-initialized');
|
||||
}, 'Player is not initialized.', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect('ready').not.toHaveBeenTriggeredOn('.video');
|
||||
});
|
||||
});
|
||||
|
||||
it('play', function() {
|
||||
initialize();
|
||||
videoPlayer.play();
|
||||
expect('play').not.toHaveBeenTriggeredOn('.video');
|
||||
});
|
||||
|
||||
it('pause', function() {
|
||||
initialize();
|
||||
videoPlayer.play();
|
||||
videoPlayer.pause();
|
||||
expect('pause').not.toHaveBeenTriggeredOn('.video');
|
||||
});
|
||||
|
||||
it('volumechange', function() {
|
||||
initialize();
|
||||
videoPlayer.onVolumeChange(60);
|
||||
|
||||
expect('volumechange').not.toHaveBeenTriggeredOn('.video');
|
||||
});
|
||||
|
||||
it('speedchange', function() {
|
||||
initialize();
|
||||
videoPlayer.onSpeedChange('2.0');
|
||||
|
||||
expect('speedchange').not.toHaveBeenTriggeredOn('.video');
|
||||
});
|
||||
|
||||
it('qualitychange', function() {
|
||||
initializeYouTube();
|
||||
videoPlayer.onPlaybackQualityChange();
|
||||
|
||||
expect('qualitychange').not.toHaveBeenTriggeredOn('.video');
|
||||
});
|
||||
|
||||
it('seek', function() {
|
||||
initialize();
|
||||
videoPlayer.onCaptionSeek({
|
||||
time: 1,
|
||||
type: 'any'
|
||||
});
|
||||
|
||||
expect('seek').not.toHaveBeenTriggeredOn('.video');
|
||||
});
|
||||
|
||||
it('ended', function() {
|
||||
initialize();
|
||||
videoPlayer.onEnded();
|
||||
|
||||
expect('ended').not.toHaveBeenTriggeredOn('.video');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -60,7 +60,6 @@
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('video_html5.html');
|
||||
this.stubVideoPlayer = jasmine.createSpy('VideoPlayer');
|
||||
$.cookie.andReturn('0.75');
|
||||
});
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@
|
||||
beforeEach(function () {
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine
|
||||
.createSpy('onTouchBasedDevice').andReturn(false);
|
||||
initialize();
|
||||
player.config.events.onReady = jasmine.createSpy('onReady');
|
||||
.createSpy('onTouchBasedDevice').andReturn(null);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -24,40 +22,119 @@
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
});
|
||||
|
||||
describe('events:', function () {
|
||||
describe('on non-Touch devices', function () {
|
||||
beforeEach(function () {
|
||||
spyOn(player, 'callStateChangeCallback').andCallThrough();
|
||||
initialize();
|
||||
player.config.events.onReady = jasmine.createSpy('onReady');
|
||||
});
|
||||
|
||||
describe('[click]', function () {
|
||||
describe('when player is paused', function () {
|
||||
describe('events:', function () {
|
||||
beforeEach(function () {
|
||||
spyOn(player, 'callStateChangeCallback').andCallThrough();
|
||||
});
|
||||
|
||||
describe('[click]', function () {
|
||||
describe('when player is paused', function () {
|
||||
beforeEach(function () {
|
||||
spyOn(player.video, 'play').andCallThrough();
|
||||
player.playerState = STATUS.PAUSED;
|
||||
$(player.videoEl).trigger('click');
|
||||
});
|
||||
|
||||
it('native play event was called', function () {
|
||||
expect(player.video.play).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('player state was changed', function () {
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.PAUSED;
|
||||
}, 'Player state should be changed', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(player.getPlayerState())
|
||||
.toBe(STATUS.PLAYING);
|
||||
});
|
||||
});
|
||||
|
||||
it('callback was called', function () {
|
||||
waitsFor(function () {
|
||||
var stateStatus = state.videoPlayer.player
|
||||
.getPlayerState();
|
||||
|
||||
return stateStatus !== STATUS.PAUSED;
|
||||
}, 'Player state should be changed', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(player.callStateChangeCallback)
|
||||
.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('[player is playing]', function () {
|
||||
beforeEach(function () {
|
||||
spyOn(player.video, 'pause').andCallThrough();
|
||||
player.playerState = STATUS.PLAYING;
|
||||
$(player.videoEl).trigger('click');
|
||||
});
|
||||
|
||||
it('native event was called', function () {
|
||||
expect(player.video.pause).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('player state was changed', function () {
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.PLAYING;
|
||||
}, 'Player state should be changed', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(player.getPlayerState())
|
||||
.toBe(STATUS.PAUSED);
|
||||
});
|
||||
});
|
||||
|
||||
it('callback was called', function () {
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.PLAYING;
|
||||
}, 'Player state should be changed', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(player.callStateChangeCallback)
|
||||
.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('[play]', function () {
|
||||
beforeEach(function () {
|
||||
spyOn(player.video, 'play').andCallThrough();
|
||||
player.playerState = STATUS.PAUSED;
|
||||
$(player.videoEl).trigger('click');
|
||||
player.playVideo();
|
||||
});
|
||||
|
||||
it('native play event was called', function () {
|
||||
it('native event was called', function () {
|
||||
expect(player.video.play).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('player state was changed', function () {
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.PAUSED;
|
||||
var state = player.getPlayerState();
|
||||
|
||||
return state !== STATUS.PAUSED;
|
||||
}, 'Player state should be changed', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(player.getPlayerState())
|
||||
.toBe(STATUS.PLAYING);
|
||||
expect(player.getPlayerState()).toBe(STATUS.PLAYING);
|
||||
});
|
||||
});
|
||||
|
||||
it('callback was called', function () {
|
||||
waitsFor(function () {
|
||||
var stateStatus = state.videoPlayer.player
|
||||
.getPlayerState();
|
||||
var state = player.getPlayerState();
|
||||
|
||||
return stateStatus !== STATUS.PAUSED;
|
||||
return state !== STATUS.PAUSED;
|
||||
}, 'Player state should be changed', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
@@ -67,11 +144,15 @@
|
||||
});
|
||||
});
|
||||
|
||||
describe('[player is playing]', function () {
|
||||
describe('[pause]', function () {
|
||||
beforeEach(function () {
|
||||
spyOn(player.video, 'pause').andCallThrough();
|
||||
player.playerState = STATUS.PLAYING;
|
||||
$(player.videoEl).trigger('click');
|
||||
player.playerState = STATUS.UNSTARTED;
|
||||
player.playVideo();
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.UNSTARTED;
|
||||
}, 'Video never started playing', WAIT_TIMEOUT);
|
||||
player.pauseVideo();
|
||||
});
|
||||
|
||||
it('native event was called', function () {
|
||||
@@ -84,8 +165,7 @@
|
||||
}, 'Player state should be changed', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(player.getPlayerState())
|
||||
.toBe(STATUS.PAUSED);
|
||||
expect(player.getPlayerState()).toBe(STATUS.PAUSED);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,243 +173,189 @@
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.PLAYING;
|
||||
}, 'Player state should be changed', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(player.callStateChangeCallback)
|
||||
.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('[play]', function () {
|
||||
beforeEach(function () {
|
||||
spyOn(player.video, 'play').andCallThrough();
|
||||
player.playerState = STATUS.PAUSED;
|
||||
player.playVideo();
|
||||
});
|
||||
describe('[loadedmetadata]', function () {
|
||||
it(
|
||||
'player state was changed, start/end was defined, ' +
|
||||
'onReady called', function ()
|
||||
{
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.UNSTARTED;
|
||||
}, 'Video cannot be played', WAIT_TIMEOUT);
|
||||
|
||||
it('native event was called', function () {
|
||||
expect(player.video.play).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('player state was changed', function () {
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.PAUSED;
|
||||
}, 'Player state should be changed', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(player.getPlayerState()).toBe(STATUS.PLAYING);
|
||||
runs(function () {
|
||||
expect(player.getPlayerState()).toBe(STATUS.PAUSED);
|
||||
expect(player.video.currentTime).toBe(0);
|
||||
expect(player.config.events.onReady)
|
||||
.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('callback was called', function () {
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.PAUSED;
|
||||
}, 'Player state should be changed', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(player.callStateChangeCallback)
|
||||
.toHaveBeenCalled();
|
||||
describe('[ended]', function () {
|
||||
beforeEach(function () {
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.UNSTARTED;
|
||||
}, 'Video cannot be played', WAIT_TIMEOUT);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('[pause]', function () {
|
||||
beforeEach(function () {
|
||||
spyOn(player.video, 'pause').andCallThrough();
|
||||
player.playerState = STATUS.UNSTARTED;
|
||||
player.playVideo();
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.UNSTARTED;
|
||||
}, 'Video never started playing', WAIT_TIMEOUT);
|
||||
player.pauseVideo();
|
||||
});
|
||||
|
||||
it('native event was called', function () {
|
||||
expect(player.video.pause).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('player state was changed', function () {
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.PLAYING;
|
||||
}, 'Player state should be changed', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(player.getPlayerState()).toBe(STATUS.PAUSED);
|
||||
it('player state was changed', function () {
|
||||
runs(function () {
|
||||
jasmine.fireEvent(player.video, 'ended');
|
||||
expect(player.getPlayerState()).toBe(STATUS.ENDED);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('callback was called', function () {
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.PLAYING;
|
||||
}, 'Player state should be changed', WAIT_TIMEOUT);
|
||||
runs(function () {
|
||||
expect(player.callStateChangeCallback)
|
||||
.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('[canplay]', function () {
|
||||
it(
|
||||
'player state was changed, start/end was defined, ' +
|
||||
'onReady called', function ()
|
||||
{
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.UNSTARTED;
|
||||
}, 'Video cannot be played', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(player.getPlayerState()).toBe(STATUS.PAUSED);
|
||||
expect(player.video.currentTime).toBe(0);
|
||||
expect(player.config.events.onReady)
|
||||
.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('[ended]', function () {
|
||||
beforeEach(function () {
|
||||
waitsFor(function () {
|
||||
return player.getPlayerState() !== STATUS.UNSTARTED;
|
||||
}, 'Video cannot be played', WAIT_TIMEOUT);
|
||||
});
|
||||
|
||||
it('player state was changed', function () {
|
||||
runs(function () {
|
||||
it('callback was called', function () {
|
||||
jasmine.fireEvent(player.video, 'ended');
|
||||
expect(player.callStateChangeCallback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', function () {
|
||||
var volume, seek, duration, playbackRate;
|
||||
|
||||
beforeEach(function () {
|
||||
waitsFor(function () {
|
||||
volume = player.video.volume;
|
||||
seek = player.video.currentTime;
|
||||
return player.playerState === STATUS.PAUSED;
|
||||
}, 'Video cannot be played', WAIT_TIMEOUT);
|
||||
});
|
||||
|
||||
it('pauseVideo', function () {
|
||||
runs(function () {
|
||||
spyOn(player.video, 'pause').andCallThrough();
|
||||
player.pauseVideo();
|
||||
expect(player.video.pause).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('seekTo', function () {
|
||||
it('set new correct value', function () {
|
||||
runs(function () {
|
||||
player.seekTo(2);
|
||||
expect(player.getCurrentTime()).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('set new inccorrect values', function () {
|
||||
runs(function () {
|
||||
player.seekTo(-50);
|
||||
expect(player.getCurrentTime()).toBe(seek);
|
||||
player.seekTo('5');
|
||||
expect(player.getCurrentTime()).toBe(seek);
|
||||
player.seekTo(500000);
|
||||
expect(player.getCurrentTime()).toBe(seek);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setVolume', function () {
|
||||
it('set new correct value', function () {
|
||||
runs(function () {
|
||||
player.setVolume(50);
|
||||
expect(player.getVolume()).toBe(50 * 0.01);
|
||||
});
|
||||
});
|
||||
|
||||
it('set new incorrect values', function () {
|
||||
runs(function () {
|
||||
player.setVolume(-50);
|
||||
expect(player.getVolume()).toBe(volume);
|
||||
player.setVolume('5');
|
||||
expect(player.getVolume()).toBe(volume);
|
||||
player.setVolume(500000);
|
||||
expect(player.getVolume()).toBe(volume);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('getCurrentTime', function () {
|
||||
runs(function () {
|
||||
player.video.currentTime = 3;
|
||||
expect(player.getCurrentTime())
|
||||
.toBe(player.video.currentTime);
|
||||
});
|
||||
});
|
||||
|
||||
it('playVideo', function () {
|
||||
runs(function () {
|
||||
spyOn(player.video, 'play').andCallThrough();
|
||||
player.playVideo();
|
||||
expect(player.video.play).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('getPlayerState', function () {
|
||||
runs(function () {
|
||||
player.playerState = STATUS.PLAYING;
|
||||
expect(player.getPlayerState()).toBe(STATUS.PLAYING);
|
||||
player.playerState = STATUS.ENDED;
|
||||
expect(player.getPlayerState()).toBe(STATUS.ENDED);
|
||||
});
|
||||
});
|
||||
|
||||
it('callback was called', function () {
|
||||
jasmine.fireEvent(player.video, 'ended');
|
||||
expect(player.callStateChangeCallback).toHaveBeenCalled();
|
||||
it('getVolume', function () {
|
||||
runs(function () {
|
||||
volume = player.video.volume = 0.5;
|
||||
expect(player.getVolume()).toBe(volume);
|
||||
});
|
||||
});
|
||||
|
||||
it('getDuration', function () {
|
||||
runs(function () {
|
||||
duration = player.video.duration;
|
||||
expect(player.getDuration()).toBe(duration);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setPlaybackRate', function () {
|
||||
it('set a correct value', function () {
|
||||
playbackRate = 1.5;
|
||||
player.setPlaybackRate(playbackRate);
|
||||
expect(player.video.playbackRate).toBe(playbackRate);
|
||||
});
|
||||
|
||||
it('set NaN value', function () {
|
||||
var oldPlaybackRate = player.video.playbackRate;
|
||||
|
||||
// When we try setting the playback rate to some
|
||||
// non-numerical value, nothing should happen.
|
||||
playbackRate = NaN;
|
||||
player.setPlaybackRate(playbackRate);
|
||||
expect(player.video.playbackRate).toBe(oldPlaybackRate);
|
||||
});
|
||||
});
|
||||
|
||||
it('getAvailablePlaybackRates', function () {
|
||||
expect(player.getAvailablePlaybackRates())
|
||||
.toEqual(playbackRates);
|
||||
});
|
||||
|
||||
it('_getLogs', function () {
|
||||
runs(function () {
|
||||
var logs = player._getLogs();
|
||||
expect(logs).toEqual(jasmine.any(Array));
|
||||
expect(logs.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', function () {
|
||||
var volume, seek, duration, playbackRate;
|
||||
it('native controls are used on iPhone', function () {
|
||||
window.onTouchBasedDevice.andReturn(['iPhone']);
|
||||
initialize();
|
||||
player.config.events.onReady = jasmine.createSpy('onReady');
|
||||
|
||||
beforeEach(function () {
|
||||
waitsFor(function () {
|
||||
volume = player.video.volume;
|
||||
seek = player.video.currentTime;
|
||||
return player.playerState === STATUS.PAUSED;
|
||||
}, 'Video cannot be played', WAIT_TIMEOUT);
|
||||
});
|
||||
|
||||
it('pauseVideo', function () {
|
||||
runs(function () {
|
||||
spyOn(player.video, 'pause').andCallThrough();
|
||||
player.pauseVideo();
|
||||
expect(player.video.pause).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('seekTo', function () {
|
||||
it('set new correct value', function () {
|
||||
runs(function () {
|
||||
player.seekTo(2);
|
||||
expect(player.getCurrentTime()).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('set new inccorrect values', function () {
|
||||
runs(function () {
|
||||
player.seekTo(-50);
|
||||
expect(player.getCurrentTime()).toBe(seek);
|
||||
player.seekTo('5');
|
||||
expect(player.getCurrentTime()).toBe(seek);
|
||||
player.seekTo(500000);
|
||||
expect(player.getCurrentTime()).toBe(seek);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setVolume', function () {
|
||||
it('set new correct value', function () {
|
||||
runs(function () {
|
||||
player.setVolume(50);
|
||||
expect(player.getVolume()).toBe(50 * 0.01);
|
||||
});
|
||||
});
|
||||
|
||||
it('set new incorrect values', function () {
|
||||
runs(function () {
|
||||
player.setVolume(-50);
|
||||
expect(player.getVolume()).toBe(volume);
|
||||
player.setVolume('5');
|
||||
expect(player.getVolume()).toBe(volume);
|
||||
player.setVolume(500000);
|
||||
expect(player.getVolume()).toBe(volume);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('getCurrentTime', function () {
|
||||
runs(function () {
|
||||
player.video.currentTime = 3;
|
||||
expect(player.getCurrentTime())
|
||||
.toBe(player.video.currentTime);
|
||||
});
|
||||
});
|
||||
|
||||
it('playVideo', function () {
|
||||
runs(function () {
|
||||
spyOn(player.video, 'play').andCallThrough();
|
||||
player.playVideo();
|
||||
expect(player.video.play).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('getPlayerState', function () {
|
||||
runs(function () {
|
||||
player.playerState = STATUS.PLAYING;
|
||||
expect(player.getPlayerState()).toBe(STATUS.PLAYING);
|
||||
player.playerState = STATUS.ENDED;
|
||||
expect(player.getPlayerState()).toBe(STATUS.ENDED);
|
||||
});
|
||||
});
|
||||
|
||||
it('getVolume', function () {
|
||||
runs(function () {
|
||||
volume = player.video.volume = 0.5;
|
||||
expect(player.getVolume()).toBe(volume);
|
||||
});
|
||||
});
|
||||
|
||||
it('getDuration', function () {
|
||||
runs(function () {
|
||||
duration = player.video.duration;
|
||||
expect(player.getDuration()).toBe(duration);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setPlaybackRate', function () {
|
||||
it('set a correct value', function () {
|
||||
playbackRate = 1.5;
|
||||
player.setPlaybackRate(playbackRate);
|
||||
expect(player.video.playbackRate).toBe(playbackRate);
|
||||
});
|
||||
|
||||
it('set NaN value', function () {
|
||||
var oldPlaybackRate = player.video.playbackRate;
|
||||
|
||||
// When we try setting the playback rate to some
|
||||
// non-numerical value, nothing should happen.
|
||||
playbackRate = NaN;
|
||||
player.setPlaybackRate(playbackRate);
|
||||
expect(player.video.playbackRate).toBe(oldPlaybackRate);
|
||||
});
|
||||
});
|
||||
|
||||
it('getAvailablePlaybackRates', function () {
|
||||
expect(player.getAvailablePlaybackRates())
|
||||
.toEqual(playbackRates);
|
||||
});
|
||||
expect($('video')).toHaveAttr('controls');
|
||||
});
|
||||
});
|
||||
}).call(this);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
beforeEach(function () {
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice')
|
||||
.andReturn(false);
|
||||
.andReturn(null);
|
||||
initialize();
|
||||
});
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
|
||||
describe('when on a touch-based device', function () {
|
||||
beforeEach(function () {
|
||||
window.onTouchBasedDevice.andReturn(true);
|
||||
window.onTouchBasedDevice.andReturn(['iPad']);
|
||||
initialize();
|
||||
});
|
||||
|
||||
@@ -209,34 +209,15 @@
|
||||
});
|
||||
|
||||
describe('mouse movement', function () {
|
||||
// We will store default window.setTimeout() function here.
|
||||
var oldSetTimeout = null;
|
||||
|
||||
beforeEach(function () {
|
||||
// Store original window.setTimeout() function. If we do not do
|
||||
// this, then all other tests that rely on code which uses
|
||||
// window.setTimeout() function might (and probably will) fail.
|
||||
oldSetTimeout = window.setTimeout;
|
||||
// Redefine window.setTimeout() function as a spy.
|
||||
window.setTimeout = jasmine.createSpy().andCallFake(
|
||||
function (callback, timeout) {
|
||||
return 5;
|
||||
}
|
||||
);
|
||||
window.setTimeout.andReturn(100);
|
||||
jasmine.Clock.useMock();
|
||||
spyOn(window, 'clearTimeout');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
// Reset the default window.setTimeout() function. If we do not
|
||||
// do this, then all other tests that rely on code which uses
|
||||
// window.setTimeout() function might (and probably will) fail.
|
||||
window.setTimeout = oldSetTimeout;
|
||||
});
|
||||
|
||||
describe('when cursor is outside of the caption box', function () {
|
||||
beforeEach(function () {
|
||||
$(window).trigger(jQuery.Event('mousemove'));
|
||||
jasmine.Clock.tick(state.config.captionsFreezeTime);
|
||||
});
|
||||
|
||||
it('does not set freezing timeout', function () {
|
||||
@@ -246,11 +227,14 @@
|
||||
|
||||
describe('when cursor is in the caption box', function () {
|
||||
beforeEach(function () {
|
||||
spyOn(videoCaption, 'onMouseLeave');
|
||||
$('.subtitles').trigger(jQuery.Event('mouseenter'));
|
||||
jasmine.Clock.tick(state.config.captionsFreezeTime);
|
||||
});
|
||||
|
||||
it('set the freezing timeout', function () {
|
||||
expect(videoCaption.frozen).toEqual(100);
|
||||
expect(videoCaption.frozen).not.toBeFalsy();
|
||||
expect(videoCaption.onMouseLeave).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when the cursor is moving', function () {
|
||||
@@ -259,7 +243,7 @@
|
||||
});
|
||||
|
||||
it('reset the freezing timeout', function () {
|
||||
expect(window.clearTimeout).toHaveBeenCalledWith(100);
|
||||
expect(window.clearTimeout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -269,7 +253,7 @@
|
||||
});
|
||||
|
||||
it('reset the freezing timeout', function () {
|
||||
expect(window.clearTimeout).toHaveBeenCalledWith(100);
|
||||
expect(window.clearTimeout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -337,7 +321,7 @@
|
||||
describe('play', function () {
|
||||
describe('when the caption was not rendered', function () {
|
||||
beforeEach(function () {
|
||||
window.onTouchBasedDevice.andReturn(true);
|
||||
window.onTouchBasedDevice.andReturn(['iPad']);
|
||||
initialize();
|
||||
videoCaption.play();
|
||||
});
|
||||
|
||||
@@ -2,15 +2,23 @@
|
||||
describe('VideoControl', function() {
|
||||
var state, videoControl, oldOTBD;
|
||||
|
||||
function initialize() {
|
||||
loadFixtures('video_all.html');
|
||||
function initialize(fixture) {
|
||||
if (fixture) {
|
||||
loadFixtures(fixture);
|
||||
} else {
|
||||
loadFixtures('video_all.html');
|
||||
}
|
||||
state = new Video('#example');
|
||||
videoControl = state.videoControl;
|
||||
}
|
||||
|
||||
function initializeYouTube() {
|
||||
initialize('video.html');
|
||||
}
|
||||
|
||||
beforeEach(function(){
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
|
||||
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(null);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -75,13 +83,13 @@
|
||||
|
||||
describe('when on a touch based device', function() {
|
||||
beforeEach(function() {
|
||||
window.onTouchBasedDevice.andReturn(true);
|
||||
window.onTouchBasedDevice.andReturn(['iPad']);
|
||||
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');
|
||||
expect($('.video_control')).toHaveClass('play');
|
||||
expect($('.video_control')).toHaveAttr('title', 'Play');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -147,6 +155,136 @@
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Play placeholder', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
this.oldYT = window.YT;
|
||||
|
||||
jasmine.stubRequests();
|
||||
window.YT = {
|
||||
Player: function () { },
|
||||
PlayerState: this.oldYT.PlayerState,
|
||||
ready: function (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
spyOn(window.YT, 'Player');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
window.YT = this.oldYT;
|
||||
});
|
||||
|
||||
|
||||
it ('works correctly on calling proper methods', function () {
|
||||
initialize();
|
||||
var btnPlay = state.el.find('.btn-play');
|
||||
|
||||
videoControl.showPlayPlaceholder();
|
||||
|
||||
expect(btnPlay).not.toHaveClass('is-hidden');
|
||||
expect(btnPlay).toHaveAttrs({
|
||||
'aria-hidden': 'false',
|
||||
'tabindex': 0
|
||||
});
|
||||
|
||||
videoControl.hidePlayPlaceholder();
|
||||
|
||||
expect(btnPlay).toHaveClass('is-hidden');
|
||||
expect(btnPlay).toHaveAttrs({
|
||||
'aria-hidden': 'true',
|
||||
'tabindex': -1
|
||||
});
|
||||
});
|
||||
|
||||
var cases = [
|
||||
{
|
||||
name: 'PC',
|
||||
isShown: false,
|
||||
isTouch: null
|
||||
},
|
||||
{
|
||||
name: 'iPad',
|
||||
isShown: true,
|
||||
isTouch: ['iPad']
|
||||
},
|
||||
{
|
||||
name: 'Android',
|
||||
isShown: true,
|
||||
isTouch: ['Android']
|
||||
},
|
||||
{
|
||||
name: 'iPhone',
|
||||
isShown: false,
|
||||
isTouch: ['iPhone']
|
||||
}
|
||||
];
|
||||
|
||||
$.each(cases, function(index, data) {
|
||||
var message = [
|
||||
(data.isShown) ? 'is' : 'is not',
|
||||
' shown on',
|
||||
data.name
|
||||
].join('');
|
||||
|
||||
it(message, function () {
|
||||
window.onTouchBasedDevice.andReturn(data.isTouch);
|
||||
initialize();
|
||||
var btnPlay = state.el.find('.btn-play');
|
||||
|
||||
if (data.isShown) {
|
||||
expect(btnPlay).not.toHaveClass('is-hidden');
|
||||
} else {
|
||||
expect(btnPlay).toHaveClass('is-hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$.each(['iPad', 'Android'], function(index, device) {
|
||||
it('is shown on paused video on '+ device +' in HTML5 player', function () {
|
||||
window.onTouchBasedDevice.andReturn([device]);
|
||||
initialize();
|
||||
var btnPlay = state.el.find('.btn-play');
|
||||
|
||||
videoControl.play();
|
||||
videoControl.pause();
|
||||
|
||||
expect(btnPlay).not.toHaveClass('is-hidden');
|
||||
});
|
||||
|
||||
it('is hidden on playing video on '+ device +' in HTML5 player', function () {
|
||||
window.onTouchBasedDevice.andReturn([device]);
|
||||
initialize();
|
||||
var btnPlay = state.el.find('.btn-play');
|
||||
|
||||
videoControl.play();
|
||||
|
||||
expect(btnPlay).toHaveClass('is-hidden');
|
||||
});
|
||||
|
||||
it('is hidden on paused video on '+ device +' in YouTube player', function () {
|
||||
window.onTouchBasedDevice.andReturn([device]);
|
||||
initializeYouTube();
|
||||
var btnPlay = state.el.find('.btn-play');
|
||||
|
||||
videoControl.play();
|
||||
videoControl.pause();
|
||||
|
||||
expect(btnPlay).toHaveClass('is-hidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('show', function () {
|
||||
initialize();
|
||||
var controls = state.el.find('.video-controls');
|
||||
controls.addClass('is-hidden');
|
||||
|
||||
videoControl.show();
|
||||
expect(controls).not.toHaveClass('is-hidden');
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
beforeEach(function () {
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice')
|
||||
.andReturn(false);
|
||||
.andReturn(null);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@@ -119,8 +119,8 @@
|
||||
window.YT = {
|
||||
Player: function () { },
|
||||
PlayerState: oldYT.PlayerState,
|
||||
ready: function (f) {
|
||||
f();
|
||||
ready: function (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -156,19 +156,18 @@
|
||||
// available globally. It is defined within the scope of Require
|
||||
// JS.
|
||||
|
||||
describe('when not on a touch based device', function () {
|
||||
beforeEach(function () {
|
||||
window.onTouchBasedDevice.andReturn(true);
|
||||
initialize();
|
||||
});
|
||||
|
||||
it('create video volume control', function () {
|
||||
expect(videoVolumeControl).toBeDefined();
|
||||
expect(videoVolumeControl.el).toHaveClass('volume');
|
||||
describe('when on a touch based device', function () {
|
||||
$.each(['iPad', 'Android'], function(index, device) {
|
||||
it('create video volume control on' + device, function() {
|
||||
window.onTouchBasedDevice.andReturn([device]);
|
||||
initialize();
|
||||
expect(videoVolumeControl).toBeUndefined();
|
||||
expect(state.el.find('div.volume')).not.toExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when on a touch based device', function () {
|
||||
describe('when not on a touch based device', function () {
|
||||
var oldOTBD;
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -343,16 +342,8 @@
|
||||
state.videoPlayer.play();
|
||||
|
||||
waitsFor(function () {
|
||||
var duration = videoPlayer.duration(),
|
||||
currentTime = videoPlayer.currentTime;
|
||||
|
||||
return (
|
||||
isFinite(currentTime) &&
|
||||
currentTime > 0 &&
|
||||
isFinite(duration) &&
|
||||
duration > 0
|
||||
);
|
||||
}, 'video begins playing', 10000);
|
||||
return videoPlayer.isPlaying();
|
||||
}, 'video begins playing', WAIT_TIMEOUT);
|
||||
});
|
||||
|
||||
it('Slider event causes log update', function () {
|
||||
@@ -555,34 +546,24 @@
|
||||
});
|
||||
|
||||
it('video is paused on first endTime, start & end time are reset', function () {
|
||||
var checkForStartEndTimeSet = true;
|
||||
var duration;
|
||||
|
||||
videoProgressSlider.notifyThroughHandleEnd.reset();
|
||||
videoPlayer.pause.reset();
|
||||
videoPlayer.play();
|
||||
|
||||
waitsFor(function () {
|
||||
if (
|
||||
!isFinite(videoPlayer.currentTime) ||
|
||||
videoPlayer.currentTime <= 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
duration = Math.round(videoPlayer.currentTime);
|
||||
|
||||
if (checkForStartEndTimeSet) {
|
||||
checkForStartEndTimeSet = false;
|
||||
|
||||
expect(videoPlayer.startTime).toBe(START_TIME);
|
||||
expect(videoPlayer.endTime).toBe(END_TIME);
|
||||
}
|
||||
|
||||
return videoPlayer.pause.calls.length === 1
|
||||
}, 5000, 'pause() has been called');
|
||||
return videoPlayer.pause.calls.length === 1;
|
||||
}, 'pause() has been called', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(videoPlayer.startTime).toBe(0);
|
||||
expect(videoPlayer.endTime).toBe(null);
|
||||
|
||||
expect(duration).toBe(END_TIME);
|
||||
|
||||
expect(videoProgressSlider.notifyThroughHandleEnd)
|
||||
.toHaveBeenCalledWith({end: true});
|
||||
});
|
||||
@@ -608,7 +589,7 @@
|
||||
}
|
||||
|
||||
return false;
|
||||
}, 'Video is fully loaded.', 1000);
|
||||
}, 'Video is fully loaded.', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
var htmlStr;
|
||||
@@ -637,7 +618,7 @@
|
||||
it('update the playback time on caption', function () {
|
||||
waitsFor(function () {
|
||||
return videoPlayer.duration() > 0;
|
||||
}, 'Video is fully loaded.', 1000);
|
||||
}, 'Video is fully loaded.', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
videoPlayer.updatePlayTime(60);
|
||||
@@ -654,7 +635,7 @@
|
||||
duration = videoPlayer.duration();
|
||||
|
||||
return duration > 0;
|
||||
}, 'Video is fully loaded.', 1000);
|
||||
}, 'Video is fully loaded.', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
videoPlayer.updatePlayTime(60);
|
||||
@@ -692,9 +673,9 @@
|
||||
waitsFor(function () {
|
||||
duration = videoPlayer.duration();
|
||||
|
||||
return duration > 0 &&
|
||||
return videoPlayer.isPlaying() &&
|
||||
videoPlayer.initialSeekToStartTime === false;
|
||||
}, 'duration becomes available', 1000);
|
||||
}, 'duration becomes available', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(videoPlayer.startTime).toBe(START_TIME);
|
||||
@@ -724,11 +705,9 @@
|
||||
videoPlayer.play();
|
||||
|
||||
waitsFor(function () {
|
||||
duration = videoPlayer.duration();
|
||||
|
||||
return duration > 0 &&
|
||||
return videoPlayer.isPlaying() &&
|
||||
videoPlayer.initialSeekToStartTime === false;
|
||||
}, 'updatePlayTime was invoked and duration is set', 5000);
|
||||
}, 'updatePlayTime was invoked and duration is set', WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(videoPlayer.endTime).toBe(null);
|
||||
@@ -896,6 +875,62 @@
|
||||
expect(realValue).toEqual(expectedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on Touch devices', function () {
|
||||
it('`is-touch` class name is added to container', function () {
|
||||
$.each(['iPad', 'Android', 'iPhone'], function(index, device) {
|
||||
window.onTouchBasedDevice.andReturn([device]);
|
||||
initialize();
|
||||
|
||||
expect(state.el).toHaveClass('is-touch');
|
||||
});
|
||||
});
|
||||
|
||||
it('modules are not initialized on iPhone', function () {
|
||||
window.onTouchBasedDevice.andReturn(['iPhone']);
|
||||
initialize();
|
||||
|
||||
var modules = [
|
||||
videoControl, videoCaption, videoProgressSlider,
|
||||
videoSpeedControl, videoVolumeControl
|
||||
];
|
||||
|
||||
$.each(modules, function (index, module) {
|
||||
expect(module).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
$.each(['iPad', 'Android'], function(index, device) {
|
||||
var message = 'controls become visible after playing starts on ' +
|
||||
device;
|
||||
it(message, function() {
|
||||
var controls;
|
||||
window.onTouchBasedDevice.andReturn([device]);
|
||||
|
||||
runs(function () {
|
||||
initialize();
|
||||
controls = state.el.find('.video-controls');
|
||||
});
|
||||
|
||||
waitsFor(function () {
|
||||
return state.el.hasClass('is-initialized');
|
||||
},'Video is not initialized.' , WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(controls).toHaveClass('is-hidden');
|
||||
videoPlayer.play();
|
||||
});
|
||||
|
||||
waitsFor(function () {
|
||||
return videoPlayer.isPlaying();
|
||||
},'Video does not play.' , WAIT_TIMEOUT);
|
||||
|
||||
runs(function () {
|
||||
expect(controls).not.toHaveClass('is-hidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
beforeEach(function() {
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice')
|
||||
.andReturn(false);
|
||||
.andReturn(null);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -44,18 +44,23 @@
|
||||
});
|
||||
|
||||
describe('on a touch-based device', function() {
|
||||
beforeEach(function() {
|
||||
window.onTouchBasedDevice.andReturn(true);
|
||||
spyOn($.fn, 'slider').andCallThrough();
|
||||
initialize();
|
||||
});
|
||||
it('does not build the slider on iPhone', function() {
|
||||
|
||||
it('does not build the slider', function() {
|
||||
expect(videoProgressSlider.slider).toBeUndefined();
|
||||
window.onTouchBasedDevice.andReturn(['iPhone']);
|
||||
initialize();
|
||||
|
||||
expect(videoProgressSlider).toBeUndefined();
|
||||
|
||||
// We can't expect $.fn.slider not to have been called,
|
||||
// because sliders are used in other parts of Video.
|
||||
});
|
||||
$.each(['iPad', 'Android'], function(index, device) {
|
||||
it('build the slider on ' + device, function() {
|
||||
window.onTouchBasedDevice.andReturn([device]);
|
||||
initialize();
|
||||
expect(videoProgressSlider.slider).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,125 +132,58 @@
|
||||
initialize();
|
||||
spyOn($.fn, 'slider').andCallThrough();
|
||||
spyOn(videoPlayer, 'onSlideSeek').andCallThrough();
|
||||
|
||||
state.videoPlayer.play();
|
||||
|
||||
waitsFor(function () {
|
||||
var duration = videoPlayer.duration(),
|
||||
currentTime = videoPlayer.currentTime;
|
||||
|
||||
return (
|
||||
isFinite(currentTime) &&
|
||||
currentTime > 0 &&
|
||||
isFinite(duration) &&
|
||||
duration > 0
|
||||
);
|
||||
}, 'video begins playing', 10000);
|
||||
});
|
||||
|
||||
it('freeze the slider', function() {
|
||||
runs(function () {
|
||||
videoProgressSlider.onSlide(
|
||||
jQuery.Event('slide'), { value: 20 }
|
||||
);
|
||||
videoProgressSlider.onSlide(
|
||||
jQuery.Event('slide'), { value: 20 }
|
||||
);
|
||||
|
||||
expect(videoProgressSlider.frozen).toBeTruthy();
|
||||
});
|
||||
expect(videoProgressSlider.frozen).toBeTruthy();
|
||||
});
|
||||
|
||||
// Turned off test due to flakiness (11/25/13)
|
||||
xit('trigger seek event', function() {
|
||||
runs(function () {
|
||||
videoProgressSlider.onSlide(
|
||||
jQuery.Event('slide'), { value: 20 }
|
||||
);
|
||||
it('trigger seek event', function() {
|
||||
videoProgressSlider.onSlide(
|
||||
jQuery.Event('slide'), { value: 20 }
|
||||
);
|
||||
|
||||
expect(videoPlayer.onSlideSeek).toHaveBeenCalled();
|
||||
|
||||
waitsFor(function () {
|
||||
return Math.round(videoPlayer.currentTime) === 20;
|
||||
}, 'currentTime got updated', 10000);
|
||||
});
|
||||
expect(videoPlayer.onSlideSeek).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onStop', function() {
|
||||
// We will store default window.setTimeout() function here.
|
||||
var oldSetTimeout = null;
|
||||
|
||||
beforeEach(function() {
|
||||
// Store original window.setTimeout() function. If we do not do
|
||||
// this, then all other tests that rely on code which uses
|
||||
// window.setTimeout() function might (and probably will) fail.
|
||||
oldSetTimeout = window.setTimeout;
|
||||
// Redefine window.setTimeout() function as a spy.
|
||||
window.setTimeout = jasmine.createSpy()
|
||||
.andCallFake(function (callback, timeout) {
|
||||
return 5;
|
||||
});
|
||||
window.setTimeout.andReturn(100);
|
||||
jasmine.Clock.useMock();
|
||||
|
||||
initialize();
|
||||
spyOn(videoPlayer, 'onSlideSeek').andCallThrough();
|
||||
videoPlayer.play();
|
||||
|
||||
waitsFor(function () {
|
||||
var duration = videoPlayer.duration(),
|
||||
currentTime = videoPlayer.currentTime;
|
||||
|
||||
return (
|
||||
isFinite(currentTime) &&
|
||||
currentTime > 0 &&
|
||||
isFinite(duration) &&
|
||||
duration > 0
|
||||
);
|
||||
}, 'video begins playing', 10000);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
// Reset the default window.setTimeout() function. If we do not
|
||||
// do this, then all other tests that rely on code which uses
|
||||
// window.setTimeout() function might (and probably will) fail.
|
||||
window.setTimeout = oldSetTimeout;
|
||||
});
|
||||
|
||||
it('freeze the slider', function() {
|
||||
runs(function () {
|
||||
videoProgressSlider.onStop(
|
||||
jQuery.Event('stop'), { value: 20 }
|
||||
);
|
||||
videoProgressSlider.onStop(
|
||||
jQuery.Event('stop'), { value: 20 }
|
||||
);
|
||||
|
||||
expect(videoProgressSlider.frozen).toBeTruthy();
|
||||
});
|
||||
expect(videoProgressSlider.frozen).toBeTruthy();
|
||||
});
|
||||
|
||||
// Turned off test due to flakiness (11/25/13)
|
||||
xit('trigger seek event', function() {
|
||||
runs(function () {
|
||||
videoProgressSlider.onStop(
|
||||
jQuery.Event('stop'), { value: 20 }
|
||||
);
|
||||
it('trigger seek event', function() {
|
||||
videoProgressSlider.onStop(
|
||||
jQuery.Event('stop'), { value: 20 }
|
||||
);
|
||||
|
||||
expect(videoPlayer.onSlideSeek).toHaveBeenCalled();
|
||||
|
||||
waitsFor(function () {
|
||||
return Math.round(videoPlayer.currentTime) === 20;
|
||||
}, 'currentTime got updated', 10000);
|
||||
});
|
||||
expect(videoPlayer.onSlideSeek).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('set timeout to unfreeze the slider', function() {
|
||||
runs(function () {
|
||||
videoProgressSlider.onStop(
|
||||
jQuery.Event('stop'), { value: 20 }
|
||||
);
|
||||
videoProgressSlider.onStop(
|
||||
jQuery.Event('stop'), { value: 20 }
|
||||
);
|
||||
|
||||
expect(window.setTimeout).toHaveBeenCalledWith(
|
||||
jasmine.any(Function), 200
|
||||
);
|
||||
window.setTimeout.mostRecentCall.args[0]();
|
||||
expect(videoProgressSlider.frozen).toBeFalsy();
|
||||
});
|
||||
jasmine.Clock.tick(200);
|
||||
|
||||
expect(videoProgressSlider.frozen).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -317,15 +255,7 @@
|
||||
videoPlayer.play();
|
||||
|
||||
waitsFor(function () {
|
||||
var duration = videoPlayer.duration(),
|
||||
currentTime = videoPlayer.currentTime;
|
||||
|
||||
return (
|
||||
isFinite(duration) &&
|
||||
duration > 0 &&
|
||||
isFinite(currentTime) &&
|
||||
currentTime > 0
|
||||
);
|
||||
return videoPlayer.isPlaying();
|
||||
}, 'duration is set, video is playing', 5000);
|
||||
|
||||
runs(function () {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine
|
||||
.createSpy('onTouchBasedDevice')
|
||||
.andReturn(false);
|
||||
.andReturn(null);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -49,7 +49,7 @@
|
||||
'role': 'button',
|
||||
'title': 'HD off',
|
||||
'aria-disabled': 'false'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('bind the quality control', function() {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
beforeEach(function() {
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
|
||||
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(null);
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
'role': 'button',
|
||||
'title': 'Speeds',
|
||||
'aria-disabled': 'false'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('bind to change video speed link', function() {
|
||||
@@ -57,16 +57,12 @@
|
||||
});
|
||||
|
||||
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');
|
||||
$.each(['iPad', 'Android'], function(index, device) {
|
||||
it('is not rendered on' + device, function() {
|
||||
window.onTouchBasedDevice.andReturn([device]);
|
||||
initialize();
|
||||
expect(state.el.find('div.speeds')).not.toExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,7 +92,7 @@
|
||||
// 2. Speed anchor
|
||||
// 3. A number of speed entry anchors
|
||||
// 4. Volume anchor
|
||||
// If an other focusable element is inserted or if the order is changed, things will
|
||||
// If another focusable element is inserted or if the order is changed, things will
|
||||
// malfunction as a flag, state.previousFocus, is set in the 1,3,4 elements and is
|
||||
// used to determine the behavior of foucus() and blur() for the speed anchor.
|
||||
it('checks for a certain order in focusable elements in video controls', function() {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
beforeEach(function() {
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
|
||||
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(null);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -58,9 +58,9 @@
|
||||
});
|
||||
expect(sliderHandle.attr('aria-valuenow')).toBeInRange(0, 100);
|
||||
expect(sliderHandle.attr('aria-valuetext')).toBeInArray(arr);
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('add ARIA attributes to volume control', function () {
|
||||
var volumeControl = $('div.volume>a');
|
||||
expect(volumeControl).toHaveAttrs({
|
||||
@@ -121,38 +121,38 @@
|
||||
{
|
||||
range: 'muted',
|
||||
value: 0,
|
||||
expectation: 'muted'
|
||||
expectation: 'muted'
|
||||
},
|
||||
{
|
||||
range: 'in ]0,20]',
|
||||
value: 10,
|
||||
expectation: 'very low'
|
||||
expectation: 'very low'
|
||||
},
|
||||
{
|
||||
range: 'in ]20,40]',
|
||||
value: 30,
|
||||
expectation: 'low'
|
||||
expectation: 'low'
|
||||
},
|
||||
{
|
||||
range: 'in ]40,60]',
|
||||
value: 50,
|
||||
expectation: 'average'
|
||||
expectation: 'average'
|
||||
},
|
||||
{
|
||||
range: 'in ]60,80]',
|
||||
value: 70,
|
||||
expectation: 'loud'
|
||||
expectation: 'loud'
|
||||
},
|
||||
{
|
||||
range: 'in ]80,100[',
|
||||
value: 90,
|
||||
expectation: 'very loud'
|
||||
expectation: 'very loud'
|
||||
},
|
||||
{
|
||||
range: 'maximum',
|
||||
value: 100,
|
||||
expectation: 'maximum'
|
||||
}
|
||||
expectation: 'maximum'
|
||||
}
|
||||
];
|
||||
|
||||
$.each(initialData, function(index, data) {
|
||||
@@ -162,7 +162,7 @@
|
||||
value: data.value
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('changes ARIA attributes', function () {
|
||||
var sliderHandle = $('div.volume-slider>a.ui-slider-handle');
|
||||
expect(sliderHandle).toHaveAttrs({
|
||||
|
||||
@@ -44,15 +44,29 @@ function (VideoPlayer) {
|
||||
|
||||
state.initialize(element)
|
||||
.done(function () {
|
||||
// On iPhones and iPods native controls are used.
|
||||
if (/iP(hone|od)/i.test(state.isTouch[0])) {
|
||||
_hideWaitPlaceholder(state);
|
||||
state.el.trigger('initialize', arguments);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_initializeModules(state)
|
||||
.done(function () {
|
||||
state.el
|
||||
.addClass('is-initialized')
|
||||
.find('.spinner')
|
||||
.attr({
|
||||
'aria-hidden': 'true',
|
||||
'tabindex': -1
|
||||
});
|
||||
// On iPad ready state occurs just after start playing.
|
||||
// We hide controls before video starts playing.
|
||||
if (/iPad|Android/i.test(state.isTouch[0])) {
|
||||
state.el.on('play', _.once(function() {
|
||||
state.trigger('videoControl.show', null);
|
||||
}));
|
||||
} else {
|
||||
// On PC show controls immediately.
|
||||
state.trigger('videoControl.show', null);
|
||||
}
|
||||
|
||||
_hideWaitPlaceholder(state);
|
||||
state.el.trigger('initialize', arguments);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -235,6 +249,16 @@ function (VideoPlayer) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function _hideWaitPlaceholder(state) {
|
||||
state.el
|
||||
.addClass('is-initialized')
|
||||
.find('.spinner')
|
||||
.attr({
|
||||
'aria-hidden': 'true',
|
||||
'tabindex': -1
|
||||
});
|
||||
}
|
||||
|
||||
function _setConfigurations(state) {
|
||||
_configureCaptions(state);
|
||||
_setPlayerMode(state);
|
||||
@@ -242,7 +266,7 @@ function (VideoPlayer) {
|
||||
// Possible value are: 'visible', 'hiding', and 'invisible'.
|
||||
state.controlState = 'visible';
|
||||
state.controlHideTimeout = null;
|
||||
state.captionState = 'visible';
|
||||
state.captionState = 'invisible';
|
||||
state.captionHideTimeout = null;
|
||||
}
|
||||
|
||||
@@ -299,12 +323,17 @@ function (VideoPlayer) {
|
||||
// element has a CSS class 'fullscreen'.
|
||||
this.__dfd__ = $.Deferred();
|
||||
this.isFullScreen = false;
|
||||
this.isTouch = onTouchBasedDevice() || '';
|
||||
|
||||
// The parent element of the video, and the ID.
|
||||
this.el = $(element).find('.video');
|
||||
this.elVideoWrapper = this.el.find('.video-wrapper');
|
||||
this.id = this.el.attr('id').replace(/video_/, '');
|
||||
|
||||
if (this.isTouch) {
|
||||
this.el.addClass('is-touch');
|
||||
}
|
||||
|
||||
// jQuery .data() return object with keys in lower camelCase format.
|
||||
data = this.el.data();
|
||||
|
||||
|
||||
@@ -90,6 +90,10 @@ function () {
|
||||
return [0.75, 1.0, 1.25, 1.5];
|
||||
};
|
||||
|
||||
Player.prototype._getLogs = function () {
|
||||
return this.logs;
|
||||
};
|
||||
|
||||
return Player;
|
||||
|
||||
/*
|
||||
@@ -129,8 +133,10 @@ function () {
|
||||
* }
|
||||
*/
|
||||
function Player(el, config) {
|
||||
var sourceStr, _this, errorMessage;
|
||||
var isTouch = onTouchBasedDevice() || '',
|
||||
sourceStr, _this, errorMessage;
|
||||
|
||||
this.logs = [];
|
||||
// Initially we assume that el is a DOM element. If jQuery selector
|
||||
// fails to select something, we assume that el is an ID of a DOM
|
||||
// element. We try to select by ID. If jQuery fails this time, we
|
||||
@@ -214,40 +220,51 @@ function () {
|
||||
// determine what the video is currently doing.
|
||||
this.videoEl = $(this.video);
|
||||
|
||||
if (/iP(hone|od)/i.test(isTouch[0])) {
|
||||
this.videoEl.prop('controls', true);
|
||||
}
|
||||
|
||||
this.playerState = HTML5Video.PlayerState.UNSTARTED;
|
||||
|
||||
// Attach a 'click' event on the <video> element. It will cause the
|
||||
// video to pause/play.
|
||||
this.videoEl.on('click', function (event) {
|
||||
if (_this.playerState === HTML5Video.PlayerState.PAUSED) {
|
||||
_this.playVideo();
|
||||
_this.playerState = HTML5Video.PlayerState.PLAYING;
|
||||
_this.callStateChangeCallback();
|
||||
} else if (
|
||||
_this.playerState === HTML5Video.PlayerState.PLAYING
|
||||
) {
|
||||
var PlayerState = HTML5Video.PlayerState;
|
||||
|
||||
if (_this.playerState === PlayerState.PLAYING) {
|
||||
_this.pauseVideo();
|
||||
_this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
_this.playerState = PlayerState.PAUSED;
|
||||
_this.callStateChangeCallback();
|
||||
} else {
|
||||
_this.playVideo();
|
||||
_this.playerState = PlayerState.PLAYING;
|
||||
_this.callStateChangeCallback();
|
||||
}
|
||||
});
|
||||
|
||||
var events = ['loadstart', 'progress', 'suspend', 'abort', 'error',
|
||||
'emptied', 'stalled', 'play', 'pause', 'loadedmetadata',
|
||||
'loadeddata', 'waiting', 'playing', 'canplay', 'canplaythrough',
|
||||
'seeking', 'seeked', 'timeupdate', 'ended', 'ratechange',
|
||||
'durationchange', 'volumechange'
|
||||
];
|
||||
|
||||
$.each(events, function(index, eventName) {
|
||||
_this.video.addEventListener(eventName, function () {
|
||||
_this.logs.push({
|
||||
'event name': eventName,
|
||||
'state': _this.playerState
|
||||
});
|
||||
|
||||
el.trigger('html5:' + eventName, arguments);
|
||||
});
|
||||
});
|
||||
|
||||
// When the <video> tag has been processed by the browser, and it
|
||||
// is ready for playback, notify other parts of the VideoPlayer,
|
||||
// and initially pause the video.
|
||||
this.video.addEventListener('canplay', function () {
|
||||
// Because Firefox triggers 'canplay' event every time when
|
||||
// 'currentTime' property changes, we must make sure that this
|
||||
// block of code runs only once. Otherwise, this will be an
|
||||
// endless loop ('currentTime' property is changed below).
|
||||
//
|
||||
// Chrome is immune to this behavior.
|
||||
if (_this.playerState !== HTML5Video.PlayerState.UNSTARTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.video.addEventListener('loadedmetadata', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
|
||||
if ($.isFunction(_this.config.events.onReady)) {
|
||||
_this.config.events.onReady(null);
|
||||
}
|
||||
@@ -259,6 +276,10 @@ function () {
|
||||
_this.callStateChangeCallback();
|
||||
}, false);
|
||||
|
||||
this.video.addEventListener('playing', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.PLAYING;
|
||||
}, false);
|
||||
|
||||
// Register the 'pause' event.
|
||||
this.video.addEventListener('pause', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
|
||||
@@ -60,7 +60,7 @@ function (HTML5Video, Resizer) {
|
||||
// via the 'state' object. Much easier to work this way - you don't
|
||||
// have to do repeated jQuery element selects.
|
||||
function _initialize(state) {
|
||||
var youTubeId, player, videoWidth, videoHeight;
|
||||
var youTubeId, player;
|
||||
|
||||
// The function is called just once to apply pre-defined configurations
|
||||
// by student before video starts playing. Waits until the video's
|
||||
@@ -124,6 +124,24 @@ function (HTML5Video, Resizer) {
|
||||
onStateChange: state.videoPlayer.onStateChange
|
||||
}
|
||||
});
|
||||
|
||||
player = state.videoEl = state.videoPlayer.player.videoEl;
|
||||
|
||||
player[0].addEventListener('loadedmetadata', function () {
|
||||
var videoWidth = player[0].videoWidth || player.width(),
|
||||
videoHeight = player[0].videoHeight || player.height();
|
||||
|
||||
_resize(state, videoWidth, videoHeight);
|
||||
|
||||
state.trigger(
|
||||
'videoControl.updateVcrVidTime',
|
||||
{
|
||||
time: 0,
|
||||
duration: state.videoPlayer.duration()
|
||||
}
|
||||
);
|
||||
}, false);
|
||||
|
||||
} else { // if (state.videoType === 'youtube') {
|
||||
if (state.currentPlayerMode === 'flash') {
|
||||
youTubeId = state.youtubeId();
|
||||
@@ -140,11 +158,18 @@ function (HTML5Video, Resizer) {
|
||||
.onPlaybackQualityChange
|
||||
}
|
||||
});
|
||||
player = state.videoEl = state.el.find('iframe');
|
||||
videoWidth = player.attr('width') || player.width();
|
||||
videoHeight = player.attr('height') || player.height();
|
||||
|
||||
_resize(state, videoWidth, videoHeight);
|
||||
state.el.on('initialize', function () {
|
||||
var player = state.videoEl = state.el.find('iframe'),
|
||||
videoWidth = player.attr('width') || player.width(),
|
||||
videoHeight = player.attr('height') || player.height();
|
||||
|
||||
_resize(state, videoWidth, videoHeight);
|
||||
});
|
||||
}
|
||||
|
||||
if (state.isTouch) {
|
||||
dfd.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,10 +179,17 @@ function (HTML5Video, Resizer) {
|
||||
elementRatio: videoWidth/videoHeight,
|
||||
container: state.videoEl.parent()
|
||||
})
|
||||
.setMode('width')
|
||||
.callbacks.once(function() {
|
||||
state.trigger('videoCaption.resize', null);
|
||||
})
|
||||
.setMode('width');
|
||||
|
||||
// Update captions size when controls becomes visible on iPad or Android
|
||||
if (/iPad|Android/i.test(state.isTouch[0])) {
|
||||
state.el.on('controls:show', function () {
|
||||
state.trigger('videoCaption.resize', null);
|
||||
});
|
||||
}
|
||||
|
||||
$(window).bind('resize', _.debounce(state.resizer.align, 100));
|
||||
}
|
||||
@@ -229,7 +261,7 @@ function (HTML5Video, Resizer) {
|
||||
// video. `endTime` will be set to `null`, and this if statement
|
||||
// will not be executed on next runs.
|
||||
if (
|
||||
this.videoPlayer.endTime != null &&
|
||||
this.videoPlayer.endTime !== null &&
|
||||
this.videoPlayer.endTime <= this.videoPlayer.currentTime
|
||||
) {
|
||||
this.videoPlayer.pause();
|
||||
@@ -297,6 +329,8 @@ function (HTML5Video, Resizer) {
|
||||
this.videoPlayer.player[methodName](youtubeId, time);
|
||||
this.videoPlayer.updatePlayTime(time);
|
||||
}
|
||||
|
||||
this.el.trigger('speedchange', arguments);
|
||||
}
|
||||
|
||||
// Every 200 ms, if the video is playing, we call the function update, via
|
||||
@@ -343,6 +377,8 @@ function (HTML5Video, Resizer) {
|
||||
}
|
||||
|
||||
this.videoPlayer.updatePlayTime(newTime);
|
||||
|
||||
this.el.trigger('seek', arguments);
|
||||
}
|
||||
|
||||
function onEnded() {
|
||||
@@ -368,6 +404,8 @@ function (HTML5Video, Resizer) {
|
||||
// `duration`. In this case, slider doesn't reach the end point of
|
||||
// timeline.
|
||||
this.videoPlayer.updatePlayTime(time);
|
||||
|
||||
this.el.trigger('ended', arguments);
|
||||
}
|
||||
|
||||
function onPause() {
|
||||
@@ -386,6 +424,8 @@ function (HTML5Video, Resizer) {
|
||||
if (this.config.show_captions) {
|
||||
this.trigger('videoCaption.pause', null);
|
||||
}
|
||||
|
||||
this.el.trigger('pause', arguments);
|
||||
}
|
||||
|
||||
function onPlay() {
|
||||
@@ -415,6 +455,8 @@ function (HTML5Video, Resizer) {
|
||||
}
|
||||
|
||||
this.videoPlayer.ready();
|
||||
|
||||
this.el.trigger('play', arguments);
|
||||
}
|
||||
|
||||
function onUnstarted() { }
|
||||
@@ -429,22 +471,17 @@ function (HTML5Video, Resizer) {
|
||||
quality = this.videoPlayer.player.getPlaybackQuality();
|
||||
|
||||
this.trigger('videoQualityControl.onQualityChange', quality);
|
||||
|
||||
this.el.trigger('qualitychange', arguments);
|
||||
}
|
||||
|
||||
function onReady() {
|
||||
var availablePlaybackRates, baseSpeedSubs, _this,
|
||||
var _this = this,
|
||||
availablePlaybackRates, baseSpeedSubs,
|
||||
player, videoWidth, videoHeight;
|
||||
|
||||
dfd.resolve();
|
||||
|
||||
if (this.videoType === 'html5') {
|
||||
player = this.videoEl = this.videoPlayer.player.videoEl;
|
||||
videoWidth = player[0].videoWidth || player.width();
|
||||
videoHeight = player[0].videoHeight || player.height();
|
||||
|
||||
_resize(this, videoWidth, videoHeight);
|
||||
}
|
||||
|
||||
this.videoPlayer.log('load_video');
|
||||
|
||||
availablePlaybackRates = this.videoPlayer.player
|
||||
@@ -469,7 +506,7 @@ function (HTML5Video, Resizer) {
|
||||
this.currentPlayerMode === 'html5' &&
|
||||
this.videoType === 'youtube'
|
||||
) {
|
||||
if (availablePlaybackRates.length === 1) {
|
||||
if (availablePlaybackRates.length === 1 && !this.isTouch) {
|
||||
// This condition is needed in cases when Firefox version is
|
||||
// less than 20. In those versions HTML5 playback could only
|
||||
// happen at 1 speed (no speed changing). Therefore, in this
|
||||
@@ -479,14 +516,11 @@ function (HTML5Video, Resizer) {
|
||||
// have 1 speed available, we fall back to Flash.
|
||||
|
||||
_restartUsingFlash(this);
|
||||
|
||||
return;
|
||||
} else if (availablePlaybackRates.length > 1) {
|
||||
// We need to synchronize available frame rates with the ones
|
||||
// that the user specified.
|
||||
|
||||
baseSpeedSubs = this.videos['1.0'];
|
||||
_this = this;
|
||||
// this.videos is a dictionary containing various frame rates
|
||||
// and their associated subs.
|
||||
|
||||
@@ -520,10 +554,11 @@ function (HTML5Video, Resizer) {
|
||||
this.videoPlayer.player.setPlaybackRate(this.speed);
|
||||
}
|
||||
|
||||
this.el.trigger('ready', arguments);
|
||||
/* The following has been commented out to make sure autoplay is
|
||||
disabled for students.
|
||||
if (
|
||||
!onTouchBasedDevice() &&
|
||||
!this.isTouch &&
|
||||
$('.video:first').data('autoplay') === 'True'
|
||||
) {
|
||||
this.videoPlayer.play();
|
||||
@@ -735,6 +770,7 @@ function (HTML5Video, Resizer) {
|
||||
|
||||
function onVolumeChange(volume) {
|
||||
this.videoPlayer.player.setVolume(volume);
|
||||
this.el.trigger('volumechange', arguments);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -32,9 +32,12 @@ function () {
|
||||
var methodsDict = {
|
||||
exitFullScreen: exitFullScreen,
|
||||
hideControls: hideControls,
|
||||
hidePlayPlaceholder: hidePlayPlaceholder,
|
||||
pause: pause,
|
||||
play: play,
|
||||
show: show,
|
||||
showControls: showControls,
|
||||
showPlayPlaceholder: showPlayPlaceholder,
|
||||
toggleFullScreen: toggleFullScreen,
|
||||
togglePlayback: togglePlayback,
|
||||
updateVcrVidTime: updateVcrVidTime
|
||||
@@ -54,16 +57,16 @@ function () {
|
||||
|
||||
state.videoControl.sliderEl = state.videoControl.el.find('.slider');
|
||||
state.videoControl.playPauseEl = state.videoControl.el.find('.video_control');
|
||||
state.videoControl.playPlaceholder = state.el.find('.btn-play');
|
||||
state.videoControl.secondaryControlsEl = state.videoControl.el.find('.secondary-controls');
|
||||
state.videoControl.fullScreenEl = state.videoControl.el.find('.add-fullscreen');
|
||||
state.videoControl.vidTimeEl = state.videoControl.el.find('.vidtime');
|
||||
|
||||
state.videoControl.fullScreenState = false;
|
||||
state.videoControl.pause();
|
||||
|
||||
if (!onTouchBasedDevice()) {
|
||||
state.videoControl.pause();
|
||||
} else {
|
||||
state.videoControl.play();
|
||||
if (state.isTouch && state.videoType === 'html5') {
|
||||
state.videoControl.showPlayPlaceholder();
|
||||
}
|
||||
|
||||
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
|
||||
@@ -99,6 +102,13 @@ function () {
|
||||
state.videoControl.playPauseEl.on('blur', function () {
|
||||
state.previousFocus = 'playPause';
|
||||
});
|
||||
|
||||
if (/iPad|Android/i.test(state.isTouch[0])) {
|
||||
state.videoControl.playPlaceholder
|
||||
.on('click', function () {
|
||||
state.trigger('videoPlayer.play', null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
@@ -106,6 +116,11 @@ function () {
|
||||
// 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 show() {
|
||||
this.videoControl.el.removeClass('is-hidden');
|
||||
this.el.trigger('controls:show', arguments);
|
||||
}
|
||||
|
||||
function showControls(event) {
|
||||
if (!this.controlShowLock) {
|
||||
if (!this.captionsHidden) {
|
||||
@@ -157,14 +172,46 @@ function () {
|
||||
});
|
||||
}
|
||||
|
||||
function showPlayPlaceholder(event) {
|
||||
this.videoControl.playPlaceholder
|
||||
.removeClass('is-hidden')
|
||||
.attr({
|
||||
'aria-hidden': 'false',
|
||||
'tabindex': 0
|
||||
});
|
||||
}
|
||||
|
||||
function hidePlayPlaceholder(event) {
|
||||
this.videoControl.playPlaceholder
|
||||
.addClass('is-hidden')
|
||||
.attr({
|
||||
'aria-hidden': 'true',
|
||||
'tabindex': -1
|
||||
});
|
||||
}
|
||||
|
||||
function play() {
|
||||
this.videoControl.playPauseEl.removeClass('play').addClass('pause').attr('title', gettext('Pause'));
|
||||
this.videoControl.isPlaying = true;
|
||||
this.videoControl.playPauseEl
|
||||
.removeClass('play')
|
||||
.addClass('pause')
|
||||
.attr('title', gettext('Pause'));
|
||||
|
||||
if (/iPad|Android/i.test(this.isTouch[0]) && this.videoType === 'html5') {
|
||||
this.videoControl.hidePlayPlaceholder();
|
||||
}
|
||||
}
|
||||
|
||||
function pause() {
|
||||
this.videoControl.playPauseEl.removeClass('pause').addClass('play').attr('title', gettext('Play'));
|
||||
this.videoControl.isPlaying = false;
|
||||
this.videoControl.playPauseEl
|
||||
.removeClass('pause')
|
||||
.addClass('play')
|
||||
.attr('title', gettext('Play'));
|
||||
|
||||
if (/iPad|Android/i.test(this.isTouch[0]) && this.videoType === 'html5') {
|
||||
this.videoControl.showPlayPlaceholder();
|
||||
}
|
||||
}
|
||||
|
||||
function togglePlayback(event) {
|
||||
|
||||
@@ -12,6 +12,7 @@ function () {
|
||||
|
||||
// Changing quality for now only works for YouTube videos.
|
||||
if (state.videoType !== 'youtube') {
|
||||
state.el.find('a.quality_control').remove();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,12 +55,10 @@ function () {
|
||||
// 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;
|
||||
state.videoProgressSlider.el = state.videoControl.sliderEl;
|
||||
|
||||
buildSlider(state);
|
||||
_buildHandle(state);
|
||||
}
|
||||
buildSlider(state);
|
||||
_buildHandle(state);
|
||||
}
|
||||
|
||||
function _buildHandle(state) {
|
||||
|
||||
@@ -10,6 +10,13 @@ function () {
|
||||
return function (state) {
|
||||
var dfd = $.Deferred();
|
||||
|
||||
if (state.isTouch) {
|
||||
// iOS doesn't support volume change
|
||||
state.el.find('div.volume').remove();
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
}
|
||||
|
||||
state.videoVolumeControl = {};
|
||||
|
||||
_makeFunctionsPublic(state);
|
||||
|
||||
@@ -10,6 +10,13 @@ function () {
|
||||
return function (state) {
|
||||
var dfd = $.Deferred();
|
||||
|
||||
if (state.isTouch) {
|
||||
// iOS doesn't support speed change
|
||||
state.el.find('div.speeds').remove();
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
}
|
||||
|
||||
state.videoSpeedControl = {};
|
||||
|
||||
_initialize(state);
|
||||
@@ -131,7 +138,7 @@ function () {
|
||||
state.videoSpeedControl.videoSpeedsEl.find('a')
|
||||
.on('click', state.videoSpeedControl.changeVideoSpeed);
|
||||
|
||||
if (onTouchBasedDevice()) {
|
||||
if (state.isTouch) {
|
||||
state.videoSpeedControl.el.on('click', function (event) {
|
||||
// So that you can't highlight this control via a drag
|
||||
// operation, we disable the default browser actions on a
|
||||
|
||||
@@ -211,6 +211,8 @@ function () {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.videoCaption.hideCaptions(this.hide_captions);
|
||||
|
||||
// Fetch the captions file. If no file was specified, or if an error
|
||||
// occurred, then we hide the captions panel, and the "CC" button
|
||||
$.ajaxWithPrefix({
|
||||
@@ -221,7 +223,7 @@ function () {
|
||||
_this.videoCaption.start = captions.start;
|
||||
_this.videoCaption.loaded = true;
|
||||
|
||||
if (onTouchBasedDevice()) {
|
||||
if (_this.isTouch) {
|
||||
_this.videoCaption.subtitlesEl.find('li').html(
|
||||
gettext(
|
||||
'Caption will be displayed when ' +
|
||||
@@ -231,6 +233,8 @@ function () {
|
||||
} else {
|
||||
_this.videoCaption.renderCaption();
|
||||
}
|
||||
|
||||
_this.videoCaption.bindHandlers();
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
console.log('[Video info]: ERROR while fetching captions.');
|
||||
@@ -349,7 +353,8 @@ function () {
|
||||
|
||||
function renderCaption() {
|
||||
var container = $('<ol>'),
|
||||
_this = this;
|
||||
_this = this,
|
||||
autohideHtml5 = this.config.autohideHtml5;
|
||||
|
||||
this.elVideoWrapper.after(this.videoCaption.subtitlesEl);
|
||||
this.el.find('.video-controls .secondary-controls')
|
||||
@@ -357,28 +362,11 @@ function () {
|
||||
|
||||
this.videoCaption.setSubtitlesHeight();
|
||||
|
||||
if ((this.videoType === 'html5') && (this.config.autohideHtml5)) {
|
||||
this.videoCaption.fadeOutTimeout = this.config.fadeOutTimeout;
|
||||
|
||||
this.videoCaption.subtitlesEl.addClass('html5');
|
||||
this.captionHideTimeout = setTimeout(
|
||||
this.videoCaption.autoHideCaptions,
|
||||
this.videoCaption.fadeOutTimeout
|
||||
);
|
||||
} else if (!this.config.autohideHtml5) {
|
||||
if ((this.videoType === 'html5' && autohideHtml5) || !autohideHtml5) {
|
||||
this.videoCaption.fadeOutTimeout = this.config.fadeOutTimeout;
|
||||
this.videoCaption.subtitlesEl.addClass('html5');
|
||||
|
||||
this.captionHideTimeout = setTimeout(
|
||||
this.videoCaption.autoHideCaptions,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
this.videoCaption.hideCaptions(this.hide_captions);
|
||||
|
||||
this.videoCaption.bindHandlers();
|
||||
|
||||
$.each(this.videoCaption.captions, function(index, text) {
|
||||
var liEl = $('<li>');
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ $ ->
|
||||
dataType: 'json'
|
||||
|
||||
window.onTouchBasedDevice = ->
|
||||
navigator.userAgent.match /iPhone|iPod|iPad/i
|
||||
navigator.userAgent.match /iPhone|iPod|iPad|Android/i
|
||||
|
||||
$('body').addClass 'touch-based-device' if onTouchBasedDevice()
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<div
|
||||
id="video_${id}"
|
||||
class="video"
|
||||
class="video closed"
|
||||
|
||||
data-streams="${youtube_streams}"
|
||||
|
||||
@@ -48,13 +48,14 @@
|
||||
|
||||
<article class="video-wrapper">
|
||||
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
|
||||
<span tabindex="-1" class="btn-play is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
|
||||
<div class="video-player-pre"></div>
|
||||
<section class="video-player">
|
||||
<div id="${id}"></div>
|
||||
<h3 class="hidden">${_('ERROR: No playable video sources found!')}</h3>
|
||||
</section>
|
||||
<div class="video-player-post"></div>
|
||||
<section class="video-controls">
|
||||
<section class="video-controls is-hidden">
|
||||
<div class="slider" title="Video position"></div>
|
||||
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user