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