Commit 23dc10d0 by Anton Stupak

Merge pull request #1998 from edx/anton/fix-video-in-ipad

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