From b6808d3d1339741795a87d69181ae215a7be92ca Mon Sep 17 00:00:00 2001 From: polesye <s2pak.anton@gmail.com> Date: Fri, 13 Dec 2013 18:03:27 +0200 Subject: [PATCH] BLD-541: Fix video controls on iPad. --- CHANGELOG.rst | 8 + cms/djangoapps/contentstore/features/video.py | 5 +- cms/static/coffee/src/main.coffee | 2 +- .../xmodule/xmodule/css/video/display.scss | 41 +- .../xmodule/xmodule/js/fixtures/video.html | 6 +- .../xmodule/js/fixtures/video_all.html | 6 +- .../xmodule/js/fixtures/video_html5.html | 6 +- .../js/fixtures/video_no_captions.html | 6 +- .../js/fixtures/video_yt_multiple.html | 6 +- .../xmodule/js/spec/video/events_spec.js | 164 +++++++ .../xmodule/js/spec/video/general_spec.js | 1 - .../xmodule/js/spec/video/html5_video_spec.js | 416 ++++++++++-------- .../js/spec/video/video_caption_spec.js | 6 +- .../js/spec/video/video_control_spec.js | 144 +++++- .../js/spec/video/video_player_spec.js | 114 +++-- .../spec/video/video_progress_slider_spec.js | 135 ++---- .../spec/video/video_quality_control_spec.js | 4 +- .../js/spec/video/video_speed_control_spec.js | 15 +- .../spec/video/video_volume_control_spec.js | 24 +- .../xmodule/js/src/video/01_initialize.js | 45 +- .../xmodule/js/src/video/02_html5_video.js | 63 ++- .../xmodule/js/src/video/03_video_player.js | 78 +++- .../xmodule/js/src/video/04_video_control.js | 59 ++- .../js/src/video/05_video_quality_control.js | 1 + .../js/src/video/06_video_progress_slider.js | 8 +- .../js/src/video/07_video_volume_control.js | 7 + .../js/src/video/08_video_speed_control.js | 9 +- .../xmodule/js/src/video/09_video_caption.js | 28 +- lms/static/coffee/src/main.coffee | 2 +- lms/templates/video.html | 5 +- 30 files changed, 949 insertions(+), 465 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/spec/video/events_spec.js diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3a83020f6a5..1dcf960e0ed 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 for iPhone (native controls are used). + - Disable volume and speed controls for iPad. + - controls becomes visible after click on video or play placeholder to avoid + issues with YouTube API. + - Captions becomes visible just after full initialization of video player. + - Fix blinking of captions after video player initialization. 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. diff --git a/cms/djangoapps/contentstore/features/video.py b/cms/djangoapps/contentstore/features/video.py index a293b137276..2a862225841 100644 --- a/cms/djangoapps/contentstore/features/video.py +++ b/cms/djangoapps/contentstore/features/video.py @@ -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 "([^"]*)"$') diff --git a/cms/static/coffee/src/main.coffee b/cms/static/coffee/src/main.coffee index fef7ce98717..5c835b27b20 100644 --- a/cms/static/coffee/src/main.coffee +++ b/cms/static/coffee/src/main.coffee @@ -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 diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss index 36318df11b7..5f4e9d1063d 100644 --- a/common/lib/xmodule/xmodule/css/video/display.scss +++ b/common/lib/xmodule/xmodule/css/video/display.scss @@ -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%; + } + } + } + } } diff --git a/common/lib/xmodule/xmodule/js/fixtures/video.html b/common/lib/xmodule/xmodule/js/fixtures/video.html index e80bd3a0ddb..a28d10422ad 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video.html @@ -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"> diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_all.html b/common/lib/xmodule/xmodule/js/fixtures/video_all.html index e7a46e1bc25..2408835f145 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_all.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_all.html @@ -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"> diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html index fcb5a3c319d..e23b8a163d7 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html @@ -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> diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html index ceb24299e93..737cada6d4b 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html @@ -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> diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html index 6a53a33970f..83e270c7bb6 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html @@ -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"> diff --git a/common/lib/xmodule/xmodule/js/spec/video/events_spec.js b/common/lib/xmodule/xmodule/js/spec/video/events_spec.js new file mode 100644 index 00000000000..3da9dfc4424 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/events_spec.js @@ -0,0 +1,164 @@ +(function () { + describe('VideoPlayer Events', function () { + var state, videoPlayer, player, videoControl, videoCaption, + videoProgressSlider, videoSpeedControl, videoVolumeControl, + oldOTBD; + + function initialize(fixture, params) { + if (_.isString(fixture)) { + loadFixtures(fixture); + } else { + if (_.isObject(fixture)) { + params = fixture; + } + + loadFixtures('video_all.html'); + } + + if (_.isObject(params)) { + $('#example') + .find('#video_id') + .data(params); + } + + state = new Video('#example'); + + state.videoEl = $('video, iframe'); + videoPlayer = state.videoPlayer; + player = videoPlayer.player; + videoControl = state.videoControl; + videoCaption = state.videoCaption; + videoProgressSlider = state.videoProgressSlider; + videoSpeedControl = state.videoSpeedControl; + videoVolumeControl = state.videoVolumeControl; + + state.resizer = (function () { + var methods = [ + 'align', + 'alignByWidthOnly', + 'alignByHeightOnly', + 'setParams', + 'setMode' + ], + obj = {}; + + $.each(methods, function (index, method) { + obj[method] = jasmine.createSpy(method).andReturn(obj); + }); + + return obj; + }()); + } + + function initializeYouTube() { + initialize('video.html'); + } + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice') + .andReturn(null); + this.oldYT = window.YT; + + jasmine.stubRequests(); + window.YT = { + Player: function () { + return { + getPlaybackQuality: function () {} + }; + }, + PlayerState: this.oldYT.PlayerState, + ready: function (callback) { + callback(); + } + }; + }); + + afterEach(function () { + $('source').remove(); + window.onTouchBasedDevice = oldOTBD; + window.YT = this.oldYT; + }); + + it('initialize', function(){ + runs(function () { + initialize(); + }); + + waitsFor(function () { + return state.el.hasClass('is-initialized'); + }, 'Player is not initialized.', WAIT_TIMEOUT); + + runs(function () { + expect('initialize').not.toHaveBeenTriggeredOn('.video'); + }); + }); + + it('ready', function() { + runs(function () { + initialize(); + }); + + waitsFor(function () { + return state.el.hasClass('is-initialized'); + }, 'Player is not initialized.', WAIT_TIMEOUT); + + runs(function () { + expect('ready').not.toHaveBeenTriggeredOn('.video'); + }); + }); + + it('play', function() { + initialize(); + videoPlayer.play(); + expect('play').not.toHaveBeenTriggeredOn('.video'); + }); + + it('pause', function() { + initialize(); + videoPlayer.play(); + videoPlayer.pause(); + expect('pause').not.toHaveBeenTriggeredOn('.video'); + }); + + it('volumechange', function() { + initialize(); + videoPlayer.onVolumeChange(60); + + expect('volumechange').not.toHaveBeenTriggeredOn('.video'); + }); + + it('speedchange', function() { + initialize(); + videoPlayer.onSpeedChange('2.0'); + + expect('speedchange').not.toHaveBeenTriggeredOn('.video'); + }); + + it('qualitychange', function() { + initializeYouTube(); + videoPlayer.onPlaybackQualityChange(); + + expect('qualitychange').not.toHaveBeenTriggeredOn('.video'); + }); + + it('seek', function() { + initialize(); + videoPlayer.onCaptionSeek({ + time: 1, + type: 'any' + }); + + expect('seek').not.toHaveBeenTriggeredOn('.video'); + }); + + it('ended', function() { + initialize(); + videoPlayer.onEnded(); + + expect('ended').not.toHaveBeenTriggeredOn('.video'); + }); + + }); + +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js index 72b5e4e3b22..dd575104b03 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js @@ -60,7 +60,6 @@ beforeEach(function () { loadFixtures('video_html5.html'); - this.stubVideoPlayer = jasmine.createSpy('VideoPlayer'); $.cookie.andReturn('0.75'); }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js b/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js index ae2b8a276e2..6a2b0b8fad1 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js @@ -11,9 +11,7 @@ beforeEach(function () { oldOTBD = window.onTouchBasedDevice; window.onTouchBasedDevice = jasmine - .createSpy('onTouchBasedDevice').andReturn(false); - initialize(); - player.config.events.onReady = jasmine.createSpy('onReady'); + .createSpy('onTouchBasedDevice').andReturn(null); }); afterEach(function() { @@ -24,40 +22,119 @@ window.onTouchBasedDevice = oldOTBD; }); - describe('events:', function () { + describe('on non-Touch devices', function () { beforeEach(function () { - spyOn(player, 'callStateChangeCallback').andCallThrough(); + initialize(); + player.config.events.onReady = jasmine.createSpy('onReady'); }); - describe('[click]', function () { - describe('when player is paused', function () { + describe('events:', function () { + beforeEach(function () { + spyOn(player, 'callStateChangeCallback').andCallThrough(); + }); + + describe('[click]', function () { + describe('when player is paused', function () { + beforeEach(function () { + spyOn(player.video, 'play').andCallThrough(); + player.playerState = STATUS.PAUSED; + $(player.videoEl).trigger('click'); + }); + + it('native play event was called', function () { + expect(player.video.play).toHaveBeenCalled(); + }); + + it('player state was changed', function () { + waitsFor(function () { + return player.getPlayerState() !== STATUS.PAUSED; + }, 'Player state should be changed', WAIT_TIMEOUT); + + runs(function () { + expect(player.getPlayerState()) + .toBe(STATUS.PLAYING); + }); + }); + + it('callback was called', function () { + waitsFor(function () { + var stateStatus = state.videoPlayer.player + .getPlayerState(); + + return stateStatus !== STATUS.PAUSED; + }, 'Player state should be changed', WAIT_TIMEOUT); + + runs(function () { + expect(player.callStateChangeCallback) + .toHaveBeenCalled(); + }); + }); + }); + + describe('[player is playing]', function () { + beforeEach(function () { + spyOn(player.video, 'pause').andCallThrough(); + player.playerState = STATUS.PLAYING; + $(player.videoEl).trigger('click'); + }); + + it('native event was called', function () { + expect(player.video.pause).toHaveBeenCalled(); + }); + + it('player state was changed', function () { + waitsFor(function () { + return player.getPlayerState() !== STATUS.PLAYING; + }, 'Player state should be changed', WAIT_TIMEOUT); + + runs(function () { + expect(player.getPlayerState()) + .toBe(STATUS.PAUSED); + }); + }); + + it('callback was called', function () { + waitsFor(function () { + return player.getPlayerState() !== STATUS.PLAYING; + }, 'Player state should be changed', WAIT_TIMEOUT); + + runs(function () { + expect(player.callStateChangeCallback) + .toHaveBeenCalled(); + }); + }); + }); + }); + + describe('[play]', function () { beforeEach(function () { spyOn(player.video, 'play').andCallThrough(); player.playerState = STATUS.PAUSED; - $(player.videoEl).trigger('click'); + player.playVideo(); }); - it('native play event was called', function () { + it('native event was called', function () { expect(player.video.play).toHaveBeenCalled(); }); + it('player state was changed', function () { waitsFor(function () { - return player.getPlayerState() !== STATUS.PAUSED; + var state = player.getPlayerState(); + + return state !== STATUS.PAUSED; }, 'Player state should be changed', WAIT_TIMEOUT); runs(function () { - expect(player.getPlayerState()) - .toBe(STATUS.PLAYING); + expect(player.getPlayerState()).toBe(STATUS.PLAYING); }); }); it('callback was called', function () { waitsFor(function () { - var stateStatus = state.videoPlayer.player - .getPlayerState(); + var state = player.getPlayerState(); - return stateStatus !== STATUS.PAUSED; + return state !== STATUS.PAUSED; }, 'Player state should be changed', WAIT_TIMEOUT); runs(function () { @@ -67,11 +144,15 @@ }); }); - describe('[player is playing]', function () { + describe('[pause]', function () { beforeEach(function () { spyOn(player.video, 'pause').andCallThrough(); - player.playerState = STATUS.PLAYING; - $(player.videoEl).trigger('click'); + player.playerState = STATUS.UNSTARTED; + player.playVideo(); + waitsFor(function () { + return player.getPlayerState() !== STATUS.UNSTARTED; + }, 'Video never started playing', WAIT_TIMEOUT); + player.pauseVideo(); }); it('native event was called', function () { @@ -84,8 +165,7 @@ }, 'Player state should be changed', WAIT_TIMEOUT); runs(function () { - expect(player.getPlayerState()) - .toBe(STATUS.PAUSED); + expect(player.getPlayerState()).toBe(STATUS.PAUSED); }); }); @@ -93,243 +173,189 @@ waitsFor(function () { return player.getPlayerState() !== STATUS.PLAYING; }, 'Player state should be changed', WAIT_TIMEOUT); - runs(function () { expect(player.callStateChangeCallback) .toHaveBeenCalled(); }); }); }); - }); - - describe('[play]', function () { - beforeEach(function () { - spyOn(player.video, 'play').andCallThrough(); - player.playerState = STATUS.PAUSED; - player.playVideo(); - }); - - it('native event was called', function () { - expect(player.video.play).toHaveBeenCalled(); - }); - it('player state was changed', function () { - waitsFor(function () { - return player.getPlayerState() !== STATUS.PAUSED; - }, 'Player state should be changed', WAIT_TIMEOUT); + describe('[loadedmetadata]', function () { + it( + 'player state was changed, start/end was defined, ' + + 'onReady called', function () + { + waitsFor(function () { + return player.getPlayerState() !== STATUS.UNSTARTED; + }, 'Video cannot be played', WAIT_TIMEOUT); - runs(function () { - expect(player.getPlayerState()).toBe(STATUS.PLAYING); + runs(function () { + expect(player.getPlayerState()).toBe(STATUS.PAUSED); + expect(player.video.currentTime).toBe(0); + expect(player.config.events.onReady) + .toHaveBeenCalled(); + }); }); }); - it('callback was called', function () { - waitsFor(function () { - return player.getPlayerState() !== STATUS.PAUSED; - }, 'Player state should be changed', WAIT_TIMEOUT); - - runs(function () { - expect(player.callStateChangeCallback) - .toHaveBeenCalled(); + describe('[ended]', function () { + beforeEach(function () { + waitsFor(function () { + return player.getPlayerState() !== STATUS.UNSTARTED; + }, 'Video cannot be played', WAIT_TIMEOUT); }); - }); - }); - - describe('[pause]', function () { - beforeEach(function () { - spyOn(player.video, 'pause').andCallThrough(); - player.playerState = STATUS.UNSTARTED; - player.playVideo(); - waitsFor(function () { - return player.getPlayerState() !== STATUS.UNSTARTED; - }, 'Video never started playing', WAIT_TIMEOUT); - player.pauseVideo(); - }); - - it('native event was called', function () { - expect(player.video.pause).toHaveBeenCalled(); - }); - - it('player state was changed', function () { - waitsFor(function () { - return player.getPlayerState() !== STATUS.PLAYING; - }, 'Player state should be changed', WAIT_TIMEOUT); - runs(function () { - expect(player.getPlayerState()).toBe(STATUS.PAUSED); + it('player state was changed', function () { + runs(function () { + jasmine.fireEvent(player.video, 'ended'); + expect(player.getPlayerState()).toBe(STATUS.ENDED); + }); }); - }); - it('callback was called', function () { - waitsFor(function () { - return player.getPlayerState() !== STATUS.PLAYING; - }, 'Player state should be changed', WAIT_TIMEOUT); - runs(function () { - expect(player.callStateChangeCallback) - .toHaveBeenCalled(); + it('callback was called', function () { + jasmine.fireEvent(player.video, 'ended'); + expect(player.callStateChangeCallback).toHaveBeenCalled(); }); }); }); - describe('[canplay]', function () { - it( - 'player state was changed, start/end was defined, ' + - 'onReady called', function () - { - waitsFor(function () { - return player.getPlayerState() !== STATUS.UNSTARTED; - }, 'Video cannot be played', WAIT_TIMEOUT); + describe('methods', function () { + var volume, seek, duration, playbackRate; - runs(function () { - expect(player.getPlayerState()).toBe(STATUS.PAUSED); - expect(player.video.currentTime).toBe(0); - expect(player.config.events.onReady) - .toHaveBeenCalled(); - }); - }); - }); - - describe('[ended]', function () { beforeEach(function () { waitsFor(function () { - return player.getPlayerState() !== STATUS.UNSTARTED; + volume = player.video.volume; + seek = player.video.currentTime; + return player.playerState === STATUS.PAUSED; }, 'Video cannot be played', WAIT_TIMEOUT); }); - it('player state was changed', function () { + it('pauseVideo', function () { runs(function () { - jasmine.fireEvent(player.video, 'ended'); - expect(player.getPlayerState()).toBe(STATUS.ENDED); + spyOn(player.video, 'pause').andCallThrough(); + player.pauseVideo(); + expect(player.video.pause).toHaveBeenCalled(); }); }); - it('callback was called', function () { - jasmine.fireEvent(player.video, 'ended'); - expect(player.callStateChangeCallback).toHaveBeenCalled(); - }); - }); - }); + describe('seekTo', function () { + it('set new correct value', function () { + runs(function () { + player.seekTo(2); + expect(player.getCurrentTime()).toBe(2); + }); + }); - describe('methods', function () { - var volume, seek, duration, playbackRate; + it('set new inccorrect values', function () { + runs(function () { + player.seekTo(-50); + expect(player.getCurrentTime()).toBe(seek); + player.seekTo('5'); + expect(player.getCurrentTime()).toBe(seek); + player.seekTo(500000); + expect(player.getCurrentTime()).toBe(seek); + }); + }); + }); - beforeEach(function () { - waitsFor(function () { - volume = player.video.volume; - seek = player.video.currentTime; - return player.playerState === STATUS.PAUSED; - }, 'Video cannot be played', WAIT_TIMEOUT); - }); + describe('setVolume', function () { + it('set new correct value', function () { + runs(function () { + player.setVolume(50); + expect(player.getVolume()).toBe(50 * 0.01); + }); + }); - it('pauseVideo', function () { - runs(function () { - spyOn(player.video, 'pause').andCallThrough(); - player.pauseVideo(); - expect(player.video.pause).toHaveBeenCalled(); + it('set new incorrect values', function () { + runs(function () { + player.setVolume(-50); + expect(player.getVolume()).toBe(volume); + player.setVolume('5'); + expect(player.getVolume()).toBe(volume); + player.setVolume(500000); + expect(player.getVolume()).toBe(volume); + }); + }); }); - }); - describe('seekTo', function () { - it('set new correct value', function () { + it('getCurrentTime', function () { runs(function () { - player.seekTo(2); - expect(player.getCurrentTime()).toBe(2); + player.video.currentTime = 3; + expect(player.getCurrentTime()) + .toBe(player.video.currentTime); }); }); - it('set new inccorrect values', function () { + it('playVideo', function () { runs(function () { - player.seekTo(-50); - expect(player.getCurrentTime()).toBe(seek); - player.seekTo('5'); - expect(player.getCurrentTime()).toBe(seek); - player.seekTo(500000); - expect(player.getCurrentTime()).toBe(seek); + spyOn(player.video, 'play').andCallThrough(); + player.playVideo(); + expect(player.video.play).toHaveBeenCalled(); }); }); - }); - describe('setVolume', function () { - it('set new correct value', function () { + it('getPlayerState', function () { runs(function () { - player.setVolume(50); - expect(player.getVolume()).toBe(50 * 0.01); + player.playerState = STATUS.PLAYING; + expect(player.getPlayerState()).toBe(STATUS.PLAYING); + player.playerState = STATUS.ENDED; + expect(player.getPlayerState()).toBe(STATUS.ENDED); }); }); - it('set new incorrect values', function () { + it('getVolume', function () { runs(function () { - player.setVolume(-50); - expect(player.getVolume()).toBe(volume); - player.setVolume('5'); - expect(player.getVolume()).toBe(volume); - player.setVolume(500000); + volume = player.video.volume = 0.5; expect(player.getVolume()).toBe(volume); }); }); - }); - - it('getCurrentTime', function () { - runs(function () { - player.video.currentTime = 3; - expect(player.getCurrentTime()) - .toBe(player.video.currentTime); - }); - }); - it('playVideo', function () { - runs(function () { - spyOn(player.video, 'play').andCallThrough(); - player.playVideo(); - expect(player.video.play).toHaveBeenCalled(); + it('getDuration', function () { + runs(function () { + duration = player.video.duration; + expect(player.getDuration()).toBe(duration); + }); }); - }); - it('getPlayerState', function () { - runs(function () { - player.playerState = STATUS.PLAYING; - expect(player.getPlayerState()).toBe(STATUS.PLAYING); - player.playerState = STATUS.ENDED; - expect(player.getPlayerState()).toBe(STATUS.ENDED); - }); - }); + describe('setPlaybackRate', function () { + it('set a correct value', function () { + playbackRate = 1.5; + player.setPlaybackRate(playbackRate); + expect(player.video.playbackRate).toBe(playbackRate); + }); - it('getVolume', function () { - runs(function () { - volume = player.video.volume = 0.5; - expect(player.getVolume()).toBe(volume); - }); - }); + it('set NaN value', function () { + var oldPlaybackRate = player.video.playbackRate; - it('getDuration', function () { - runs(function () { - duration = player.video.duration; - expect(player.getDuration()).toBe(duration); + // When we try setting the playback rate to some + // non-numerical value, nothing should happen. + playbackRate = NaN; + player.setPlaybackRate(playbackRate); + expect(player.video.playbackRate).toBe(oldPlaybackRate); + }); }); - }); - describe('setPlaybackRate', function () { - it('set a correct value', function () { - playbackRate = 1.5; - player.setPlaybackRate(playbackRate); - expect(player.video.playbackRate).toBe(playbackRate); + it('getAvailablePlaybackRates', function () { + expect(player.getAvailablePlaybackRates()) + .toEqual(playbackRates); }); - it('set NaN value', function () { - var oldPlaybackRate = player.video.playbackRate; - - // When we try setting the playback rate to some - // non-numerical value, nothing should happen. - playbackRate = NaN; - player.setPlaybackRate(playbackRate); - expect(player.video.playbackRate).toBe(oldPlaybackRate); + it('_getLogs', function () { + runs(function () { + var logs = player._getLogs(); + expect(logs).toEqual(jasmine.any(Array)); + expect(logs.length).toBeGreaterThan(0); + }); }); }); + }); - it('getAvailablePlaybackRates', function () { - expect(player.getAvailablePlaybackRates()) - .toEqual(playbackRates); - }); + 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); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js index 4e45d328388..bdc903b191f 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js @@ -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(); }); @@ -337,7 +337,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(); }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js index 1c6912cb795..a213669bed8 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js @@ -2,15 +2,23 @@ describe('VideoControl', function() { var state, videoControl, oldOTBD; - function initialize() { - loadFixtures('video_all.html'); + function initialize(fixture) { + if (fixture) { + loadFixtures(fixture); + } else { + loadFixtures('video_all.html'); + } state = new Video('#example'); videoControl = state.videoControl; } + function initializeYouTube() { + initialize('video.html'); + } + beforeEach(function(){ oldOTBD = window.onTouchBasedDevice; - window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false); + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(null); }); afterEach(function() { @@ -75,13 +83,13 @@ describe('when on a touch based device', function() { beforeEach(function() { - window.onTouchBasedDevice.andReturn(true); + window.onTouchBasedDevice.andReturn(['iPad']); initialize(); }); it('does not add the play class to video control', function() { - expect($('.video_control')).not.toHaveClass('play'); - expect($('.video_control')).not.toHaveAttr('title', 'Play'); + expect($('.video_control')).toHaveClass('play'); + expect($('.video_control')).toHaveAttr('title', 'Play'); }); }); }); @@ -147,6 +155,130 @@ }); }); }); + + 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: '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'); + } + }); + }); + + it('is shown on paused video on iPad in HTML5 player', function () { + window.onTouchBasedDevice.andReturn(['iPad']); + 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 iPad in HTML5 player', function () { + window.onTouchBasedDevice.andReturn(['iPad']); + initialize(); + var btnPlay = state.el.find('.btn-play'); + + videoControl.play(); + + expect(btnPlay).toHaveClass('is-hidden'); + }); + + it('is hidden on paused video on iPad in YouTube player', function () { + window.onTouchBasedDevice.andReturn(['iPad']); + 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); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js index 62e14753568..bd4c1e0fbc9 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js @@ -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,19 @@ // available globally. It is defined within the scope of Require // JS. - describe('when not on a touch based device', function () { + describe('when on a touch based device', function () { beforeEach(function () { - window.onTouchBasedDevice.andReturn(true); + window.onTouchBasedDevice.andReturn(['iPad']); initialize(); }); it('create video volume control', function () { - expect(videoVolumeControl).toBeDefined(); - expect(videoVolumeControl.el).toHaveClass('volume'); + expect(videoVolumeControl).toBeUndefined(); + expect(state.el.find('div.volume')).not.toExist(); }); }); - describe('when on a touch based device', function () { + describe('when not on a touch based device', function () { var oldOTBD; beforeEach(function () { @@ -343,16 +343,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 +547,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 +590,7 @@ } return false; - }, 'Video is fully loaded.', 1000); + }, 'Video is fully loaded.', WAIT_TIMEOUT); runs(function () { var htmlStr; @@ -637,7 +619,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 +636,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 +674,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 +706,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 +876,54 @@ expect(realValue).toEqual(expectedValue); }); }); + + describe('on Touch devices', function () { + it('`is-touch` class name is added to container', function () { + window.onTouchBasedDevice.andReturn(['iPad']); + 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(); + }); + }); + + it('controls become visible after playing starts on iPad', function () { + window.onTouchBasedDevice.andReturn(['iPad']); + initialize(); + + var 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); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js index a8450c39cdf..e885cb42a60 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js @@ -12,7 +12,7 @@ beforeEach(function() { oldOTBD = window.onTouchBasedDevice; window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice') - .andReturn(false); + .andReturn(null); }); afterEach(function() { @@ -44,18 +44,22 @@ }); 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. }); + it('build the slider on iPad', function() { + window.onTouchBasedDevice.andReturn(['iPad']); + initialize(); + + expect(videoProgressSlider.slider).toBeDefined(); + }); }); }); @@ -127,45 +131,22 @@ initialize(); spyOn($.fn, 'slider').andCallThrough(); spyOn(videoPlayer, 'onSlideSeek').andCallThrough(); - - state.videoPlayer.play(); - - waitsFor(function () { - var duration = videoPlayer.duration(), - currentTime = videoPlayer.currentTime; - - return ( - isFinite(currentTime) && - currentTime > 0 && - isFinite(duration) && - duration > 0 - ); - }, 'video begins playing', 10000); }); it('freeze the slider', function() { - runs(function () { - videoProgressSlider.onSlide( - jQuery.Event('slide'), { value: 20 } - ); + videoProgressSlider.onSlide( + jQuery.Event('slide'), { value: 20 } + ); - expect(videoProgressSlider.frozen).toBeTruthy(); - }); + expect(videoProgressSlider.frozen).toBeTruthy(); }); - // Turned off test due to flakiness (11/25/13) - xit('trigger seek event', function() { - runs(function () { - videoProgressSlider.onSlide( - jQuery.Event('slide'), { value: 20 } - ); + it('trigger seek event', function() { + videoProgressSlider.onSlide( + jQuery.Event('slide'), { value: 20 } + ); - expect(videoPlayer.onSlideSeek).toHaveBeenCalled(); - - waitsFor(function () { - return Math.round(videoPlayer.currentTime) === 20; - }, 'currentTime got updated', 10000); - }); + expect(videoPlayer.onSlideSeek).toHaveBeenCalled(); }); }); @@ -179,27 +160,10 @@ // 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); + window.setTimeout = jasmine.createSpy().andReturn(100); 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 () { @@ -210,42 +174,31 @@ }); it('freeze the slider', function() { - runs(function () { - videoProgressSlider.onStop( - jQuery.Event('stop'), { value: 20 } - ); + videoProgressSlider.onStop( + jQuery.Event('stop'), { value: 20 } + ); - expect(videoProgressSlider.frozen).toBeTruthy(); - }); + expect(videoProgressSlider.frozen).toBeTruthy(); }); - // Turned off test due to flakiness (11/25/13) - xit('trigger seek event', function() { - runs(function () { - videoProgressSlider.onStop( - jQuery.Event('stop'), { value: 20 } - ); - - expect(videoPlayer.onSlideSeek).toHaveBeenCalled(); + it('trigger seek event', function() { + videoProgressSlider.onStop( + jQuery.Event('stop'), { value: 20 } + ); - waitsFor(function () { - return Math.round(videoPlayer.currentTime) === 20; - }, 'currentTime got updated', 10000); - }); + expect(videoPlayer.onSlideSeek).toHaveBeenCalled(); }); it('set timeout to unfreeze the slider', function() { - runs(function () { - videoProgressSlider.onStop( - jQuery.Event('stop'), { value: 20 } - ); - - expect(window.setTimeout).toHaveBeenCalledWith( - jasmine.any(Function), 200 - ); - window.setTimeout.mostRecentCall.args[0](); - expect(videoProgressSlider.frozen).toBeFalsy(); - }); + videoProgressSlider.onStop( + jQuery.Event('stop'), { value: 20 } + ); + + expect(window.setTimeout).toHaveBeenCalledWith( + jasmine.any(Function), 200 + ); + window.setTimeout.mostRecentCall.args[0](); + expect(videoProgressSlider.frozen).toBeFalsy(); }); }); @@ -317,15 +270,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 () { diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js index d1749b48f1b..d8bd234684c 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js @@ -13,7 +13,7 @@ oldOTBD = window.onTouchBasedDevice; window.onTouchBasedDevice = jasmine .createSpy('onTouchBasedDevice') - .andReturn(false); + .andReturn(null); }); afterEach(function() { @@ -49,7 +49,7 @@ 'role': 'button', 'title': 'HD off', 'aria-disabled': 'false' - }); + }); }); it('bind the quality control', function() { diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js index 6836b2fcf67..0ca4cde9948 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js @@ -12,7 +12,7 @@ beforeEach(function() { oldOTBD = window.onTouchBasedDevice; - window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false); + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(null); }); @@ -48,7 +48,7 @@ 'role': 'button', 'title': 'Speeds', 'aria-disabled': 'false' - }); + }); }); it('bind to change video speed link', function() { @@ -58,15 +58,12 @@ describe('when running on touch based device', function() { beforeEach(function() { - window.onTouchBasedDevice.andReturn(true); + window.onTouchBasedDevice.andReturn(['iPad']); initialize(); }); - it('open the speed toggle on click', function() { - $('.speeds').click(); - expect($('.speeds')).toHaveClass('open'); - $('.speeds').click(); - expect($('.speeds')).not.toHaveClass('open'); + it('is not rendered', function() { + expect(state.el.find('div.speeds')).not.toExist(); }); }); @@ -96,7 +93,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() { diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js index 9e64a63b4d2..c8e2db97f7c 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js @@ -11,7 +11,7 @@ beforeEach(function() { oldOTBD = window.onTouchBasedDevice; - window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false); + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(null); }); afterEach(function() { @@ -58,9 +58,9 @@ }); expect(sliderHandle.attr('aria-valuenow')).toBeInRange(0, 100); expect(sliderHandle.attr('aria-valuetext')).toBeInArray(arr); - + }); - + it('add ARIA attributes to volume control', function () { var volumeControl = $('div.volume>a'); expect(volumeControl).toHaveAttrs({ @@ -121,38 +121,38 @@ { range: 'muted', value: 0, - expectation: 'muted' + expectation: 'muted' }, { range: 'in ]0,20]', value: 10, - expectation: 'very low' + expectation: 'very low' }, { range: 'in ]20,40]', value: 30, - expectation: 'low' + expectation: 'low' }, { range: 'in ]40,60]', value: 50, - expectation: 'average' + expectation: 'average' }, { range: 'in ]60,80]', value: 70, - expectation: 'loud' + expectation: 'loud' }, { range: 'in ]80,100[', value: 90, - expectation: 'very loud' + expectation: 'very loud' }, { range: 'maximum', value: 100, - expectation: 'maximum' - } + expectation: 'maximum' + } ]; $.each(initialData, function(index, data) { @@ -162,7 +162,7 @@ value: data.value }); }); - + it('changes ARIA attributes', function () { var sliderHandle = $('div.volume-slider>a.ui-slider-handle'); expect(sliderHandle).toHaveAttrs({ diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js index 4ced6484830..77fef5ebc70 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -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(); diff --git a/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js b/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js index 26353989371..85b16ea2105 100644 --- a/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js @@ -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; diff --git a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js index 50eff093ee6..7d7472e793b 100644 --- a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js +++ b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js @@ -60,7 +60,7 @@ function (HTML5Video, Resizer) { // via the 'state' object. Much easier to work this way - you don't // have to do repeated jQuery element selects. function _initialize(state) { - var youTubeId, player, videoWidth, videoHeight; + var youTubeId, player; // The function is called just once to apply pre-defined configurations // by student before video starts playing. Waits until the video's @@ -124,6 +124,24 @@ function (HTML5Video, Resizer) { onStateChange: state.videoPlayer.onStateChange } }); + + player = state.videoEl = state.videoPlayer.player.videoEl; + + player[0].addEventListener('loadedmetadata', function () { + var videoWidth = player[0].videoWidth || player.width(), + videoHeight = player[0].videoHeight || player.height(); + + _resize(state, videoWidth, videoHeight); + + state.trigger( + 'videoControl.updateVcrVidTime', + { + time: 0, + duration: state.videoPlayer.duration() + } + ); + }, false); + } else { // if (state.videoType === 'youtube') { if (state.currentPlayerMode === 'flash') { youTubeId = state.youtubeId(); @@ -140,11 +158,18 @@ function (HTML5Video, Resizer) { .onPlaybackQualityChange } }); - player = state.videoEl = state.el.find('iframe'); - videoWidth = player.attr('width') || player.width(); - videoHeight = player.attr('height') || player.height(); - _resize(state, videoWidth, videoHeight); + state.el.on('initialize', function () { + var player = state.videoEl = state.el.find('iframe'), + videoWidth = player.attr('width') || player.width(), + videoHeight = player.attr('height') || player.height(); + + _resize(state, videoWidth, videoHeight); + }); + } + + if (state.isTouch) { + dfd.resolve(); } } @@ -154,10 +179,17 @@ function (HTML5Video, Resizer) { elementRatio: videoWidth/videoHeight, container: state.videoEl.parent() }) - .setMode('width') .callbacks.once(function() { state.trigger('videoCaption.resize', null); + }) + .setMode('width'); + + // Update captions size when controls becomes visible on iPad or Android + if (/iPad|Android/i.test(state.isTouch[0])) { + state.el.on('controls:show', function () { + state.trigger('videoCaption.resize', null); }); + } $(window).bind('resize', _.debounce(state.resizer.align, 100)); } @@ -229,7 +261,7 @@ function (HTML5Video, Resizer) { // video. `endTime` will be set to `null`, and this if statement // will not be executed on next runs. if ( - this.videoPlayer.endTime != null && + this.videoPlayer.endTime !== null && this.videoPlayer.endTime <= this.videoPlayer.currentTime ) { this.videoPlayer.pause(); @@ -297,6 +329,8 @@ function (HTML5Video, Resizer) { this.videoPlayer.player[methodName](youtubeId, time); this.videoPlayer.updatePlayTime(time); } + + this.el.trigger('speedchange', arguments); } // Every 200 ms, if the video is playing, we call the function update, via @@ -343,6 +377,8 @@ function (HTML5Video, Resizer) { } this.videoPlayer.updatePlayTime(newTime); + + this.el.trigger('seek', arguments); } function onEnded() { @@ -368,6 +404,8 @@ function (HTML5Video, Resizer) { // `duration`. In this case, slider doesn't reach the end point of // timeline. this.videoPlayer.updatePlayTime(time); + + this.el.trigger('ended', arguments); } function onPause() { @@ -386,6 +424,8 @@ function (HTML5Video, Resizer) { if (this.config.show_captions) { this.trigger('videoCaption.pause', null); } + + this.el.trigger('pause', arguments); } function onPlay() { @@ -415,6 +455,8 @@ function (HTML5Video, Resizer) { } this.videoPlayer.ready(); + + this.el.trigger('play', arguments); } function onUnstarted() { } @@ -429,22 +471,17 @@ function (HTML5Video, Resizer) { quality = this.videoPlayer.player.getPlaybackQuality(); this.trigger('videoQualityControl.onQualityChange', quality); + + this.el.trigger('qualitychange', arguments); } function onReady() { - var availablePlaybackRates, baseSpeedSubs, _this, + var _this = this, + availablePlaybackRates, baseSpeedSubs, player, videoWidth, videoHeight; dfd.resolve(); - if (this.videoType === 'html5') { - player = this.videoEl = this.videoPlayer.player.videoEl; - videoWidth = player[0].videoWidth || player.width(); - videoHeight = player[0].videoHeight || player.height(); - - _resize(this, videoWidth, videoHeight); - } - this.videoPlayer.log('load_video'); availablePlaybackRates = this.videoPlayer.player @@ -469,7 +506,7 @@ function (HTML5Video, Resizer) { this.currentPlayerMode === 'html5' && this.videoType === 'youtube' ) { - if (availablePlaybackRates.length === 1) { + if (availablePlaybackRates.length === 1 && !this.isTouch) { // This condition is needed in cases when Firefox version is // less than 20. In those versions HTML5 playback could only // happen at 1 speed (no speed changing). Therefore, in this @@ -479,14 +516,11 @@ function (HTML5Video, Resizer) { // have 1 speed available, we fall back to Flash. _restartUsingFlash(this); - - return; } else if (availablePlaybackRates.length > 1) { // We need to synchronize available frame rates with the ones // that the user specified. baseSpeedSubs = this.videos['1.0']; - _this = this; // this.videos is a dictionary containing various frame rates // and their associated subs. @@ -520,10 +554,11 @@ function (HTML5Video, Resizer) { this.videoPlayer.player.setPlaybackRate(this.speed); } + this.el.trigger('ready', arguments); /* The following has been commented out to make sure autoplay is disabled for students. if ( - !onTouchBasedDevice() && + !this.isTouch && $('.video:first').data('autoplay') === 'True' ) { this.videoPlayer.play(); @@ -735,6 +770,7 @@ function (HTML5Video, Resizer) { function onVolumeChange(volume) { this.videoPlayer.player.setVolume(volume); + this.el.trigger('volumechange', arguments); } }); diff --git a/common/lib/xmodule/xmodule/js/src/video/04_video_control.js b/common/lib/xmodule/xmodule/js/src/video/04_video_control.js index f9eae8e3838..f496ce7fd94 100644 --- a/common/lib/xmodule/xmodule/js/src/video/04_video_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/04_video_control.js @@ -32,9 +32,12 @@ function () { var methodsDict = { exitFullScreen: exitFullScreen, hideControls: hideControls, + hidePlayPlaceholder: hidePlayPlaceholder, pause: pause, play: play, + show: show, showControls: showControls, + showPlayPlaceholder: showPlayPlaceholder, toggleFullScreen: toggleFullScreen, togglePlayback: togglePlayback, updateVcrVidTime: updateVcrVidTime @@ -54,16 +57,16 @@ function () { state.videoControl.sliderEl = state.videoControl.el.find('.slider'); state.videoControl.playPauseEl = state.videoControl.el.find('.video_control'); + state.videoControl.playPlaceholder = state.el.find('.btn-play'); state.videoControl.secondaryControlsEl = state.videoControl.el.find('.secondary-controls'); state.videoControl.fullScreenEl = state.videoControl.el.find('.add-fullscreen'); state.videoControl.vidTimeEl = state.videoControl.el.find('.vidtime'); state.videoControl.fullScreenState = false; + state.videoControl.pause(); - if (!onTouchBasedDevice()) { - state.videoControl.pause(); - } else { - state.videoControl.play(); + if (state.isTouch && state.videoType === 'html5') { + state.videoControl.showPlayPlaceholder(); } if ((state.videoType === 'html5') && (state.config.autohideHtml5)) { @@ -99,6 +102,13 @@ function () { state.videoControl.playPauseEl.on('blur', function () { state.previousFocus = 'playPause'; }); + + if (/iPad|Android/i.test(state.isTouch[0])) { + state.videoControl.playPlaceholder + .on('click', function () { + state.trigger('videoPlayer.play', null); + }); + } } // *************************************************************** @@ -106,6 +116,11 @@ function () { // These are available via the 'state' object. Their context ('this' keyword) is the 'state' object. // The magic private function that makes them available and sets up their context is makeFunctionsPublic(). // *************************************************************** + function show() { + this.videoControl.el.removeClass('is-hidden'); + this.el.trigger('controls:show', arguments); + } + function showControls(event) { if (!this.controlShowLock) { if (!this.captionsHidden) { @@ -157,14 +172,46 @@ function () { }); } + function showPlayPlaceholder(event) { + this.videoControl.playPlaceholder + .removeClass('is-hidden') + .attr({ + 'aria-hidden': 'false', + 'tabindex': 0 + }); + } + + function hidePlayPlaceholder(event) { + this.videoControl.playPlaceholder + .addClass('is-hidden') + .attr({ + 'aria-hidden': 'true', + 'tabindex': -1 + }); + } + function play() { - this.videoControl.playPauseEl.removeClass('play').addClass('pause').attr('title', gettext('Pause')); this.videoControl.isPlaying = true; + this.videoControl.playPauseEl + .removeClass('play') + .addClass('pause') + .attr('title', gettext('Pause')); + + if (/iPad|Android/i.test(this.isTouch[0]) && this.videoType === 'html5') { + this.videoControl.hidePlayPlaceholder(); + } } function pause() { - this.videoControl.playPauseEl.removeClass('pause').addClass('play').attr('title', gettext('Play')); this.videoControl.isPlaying = false; + this.videoControl.playPauseEl + .removeClass('pause') + .addClass('play') + .attr('title', gettext('Play')); + + if (/iPad|Android/i.test(this.isTouch[0]) && this.videoType === 'html5') { + this.videoControl.showPlayPlaceholder(); + } } function togglePlayback(event) { diff --git a/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js b/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js index 6052622128b..5f1777ca0b2 100644 --- a/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js @@ -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; } diff --git a/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js b/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js index e10c4fbb243..29b89f10107 100644 --- a/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js +++ b/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js @@ -55,12 +55,10 @@ function () { // via the 'state' object. Much easier to work this way - you don't // have to do repeated jQuery element selects. function _renderElements(state) { - if (!onTouchBasedDevice()) { - state.videoProgressSlider.el = state.videoControl.sliderEl; + state.videoProgressSlider.el = state.videoControl.sliderEl; - buildSlider(state); - _buildHandle(state); - } + buildSlider(state); + _buildHandle(state); } function _buildHandle(state) { diff --git a/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js b/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js index dbabe5a15e5..f6b81cc1c78 100644 --- a/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js @@ -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); diff --git a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js index 9ba2da055e4..c8ceeb17703 100644 --- a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js @@ -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 diff --git a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js index 8092abe794f..a89c459ca7a 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js @@ -211,6 +211,8 @@ function () { return false; } + this.videoCaption.hideCaptions(this.hide_captions); + // Fetch the captions file. If no file was specified, or if an error // occurred, then we hide the captions panel, and the "CC" button $.ajaxWithPrefix({ @@ -221,7 +223,7 @@ function () { _this.videoCaption.start = captions.start; _this.videoCaption.loaded = true; - if (onTouchBasedDevice()) { + if (_this.isTouch) { _this.videoCaption.subtitlesEl.find('li').html( gettext( 'Caption will be displayed when ' + @@ -231,6 +233,8 @@ function () { } else { _this.videoCaption.renderCaption(); } + + _this.videoCaption.bindHandlers(); }, error: function (jqXHR, textStatus, errorThrown) { console.log('[Video info]: ERROR while fetching captions.'); @@ -349,7 +353,8 @@ function () { function renderCaption() { var container = $('<ol>'), - _this = this; + _this = this, + autohideHtml5 = this.config.autohideHtml5; this.elVideoWrapper.after(this.videoCaption.subtitlesEl); this.el.find('.video-controls .secondary-controls') @@ -357,28 +362,11 @@ function () { this.videoCaption.setSubtitlesHeight(); - if ((this.videoType === 'html5') && (this.config.autohideHtml5)) { - this.videoCaption.fadeOutTimeout = this.config.fadeOutTimeout; - - this.videoCaption.subtitlesEl.addClass('html5'); - this.captionHideTimeout = setTimeout( - this.videoCaption.autoHideCaptions, - this.videoCaption.fadeOutTimeout - ); - } else if (!this.config.autohideHtml5) { + if ((this.videoType === 'html5' && autohideHtml5) || !autohideHtml5) { this.videoCaption.fadeOutTimeout = this.config.fadeOutTimeout; this.videoCaption.subtitlesEl.addClass('html5'); - - this.captionHideTimeout = setTimeout( - this.videoCaption.autoHideCaptions, - 0 - ); } - this.videoCaption.hideCaptions(this.hide_captions); - - this.videoCaption.bindHandlers(); - $.each(this.videoCaption.captions, function(index, text) { var liEl = $('<li>'); diff --git a/lms/static/coffee/src/main.coffee b/lms/static/coffee/src/main.coffee index df4c8861f67..89b9c6c4d98 100644 --- a/lms/static/coffee/src/main.coffee +++ b/lms/static/coffee/src/main.coffee @@ -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() diff --git a/lms/templates/video.html b/lms/templates/video.html index ea547d84ec5..4656e1f3b66 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -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> -- GitLab