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,
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">
......
(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,6 +22,12 @@
window.onTouchBasedDevice = oldOTBD;
});
describe('on non-Touch devices', function () {
beforeEach(function () {
initialize();
player.config.events.onReady = jasmine.createSpy('onReady');
});
describe('events:', function () {
beforeEach(function () {
spyOn(player, 'callStateChangeCallback').andCallThrough();
......@@ -113,9 +117,12 @@
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 () {
......@@ -125,7 +132,9 @@
it('callback was called', 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 () {
......@@ -171,7 +180,7 @@
});
});
describe('[canplay]', function () {
describe('[loadedmetadata]', function () {
it(
'player state was changed, start/end was defined, ' +
'onReady called', function ()
......@@ -330,6 +339,23 @@
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);
});
});
});
});
it('native controls are used on iPhone', function () {
window.onTouchBasedDevice.andReturn(['iPhone']);
initialize();
player.config.events.onReady = jasmine.createSpy('onReady');
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);
beforeEach(function () {
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() {
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);
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();
});
it('create video volume control', function () {
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;
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();
it('does not build the slider on iPhone', function() {
window.onTouchBasedDevice.andReturn(['iPhone']);
initialize();
});
it('does not build the slider', function() {
expect(videoProgressSlider.slider).toBeUndefined();
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,127 +132,60 @@
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 }
);
expect(videoProgressSlider.frozen).toBeTruthy();
});
});
// Turned off test due to flakiness (11/25/13)
xit('trigger seek event', function() {
runs(function () {
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);
});
});
});
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 }
);
expect(videoProgressSlider.frozen).toBeTruthy();
});
});
// Turned off test due to flakiness (11/25/13)
xit('trigger seek event', function() {
runs(function () {
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);
});
});
it('set timeout to unfreeze the slider', function() {
runs(function () {
videoProgressSlider.onStop(
jQuery.Event('stop'), { value: 20 }
);
expect(window.setTimeout).toHaveBeenCalledWith(
jasmine.any(Function), 200
);
window.setTimeout.mostRecentCall.args[0]();
jasmine.Clock.tick(200);
expect(videoProgressSlider.frozen).toBeFalsy();
});
});
});
it('getRangeParams' , function() {
var testCases = [
......@@ -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() {
......
......@@ -12,7 +12,7 @@
beforeEach(function() {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(null);
});
......@@ -57,16 +57,12 @@
});
describe('when running on touch based device', function() {
beforeEach(function() {
window.onTouchBasedDevice.andReturn(true);
$.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();
});
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 @@
// 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() {
......
......@@ -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();
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;
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,13 +55,11 @@ 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;
buildSlider(state);
_buildHandle(state);
}
}
function _buildHandle(state) {
state.videoProgressSlider.handle = state.videoProgressSlider.el
......
......@@ -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)) {
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.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>
......
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