1
0
mirror of https://github.com/videojs/video.js.git synced 2025-01-27 11:22:06 +02:00

Close GH-672: Control bar updates. Fixes #556, Fixes #500, Fixes #374, Fixes #403, Fixes #441, Fixes #193, Fixes #602, Fixes #561, Fixes #281

This commit is contained in:
Andy Niccolai 2013-08-09 14:29:22 -07:00 committed by Steve Heffernan
parent 699c476575
commit 02de927043
17 changed files with 652 additions and 245 deletions

View File

@ -4,20 +4,21 @@ Version GENERATED_AT_BUILD
Create your own skin at http://designer.videojs.com
*/
// To customize the player skin, change the values of the variables or edit the
// To customize the player skin, change the values of the variables or edit the
// CSS below.
// (This file uses LESS. Learn more at http://lesscss.org/)
// The base font size controls the size of everything, not just text. All
// The base font size controls the size of everything, not just text. All
// diminensions use em-based sizes so that the scale along with the font size.
// Try increasing it to 20px and see what happens.
@base-font-size: 10px;
@touch-device-font-size: 15px;
// The main font color controls the color of the text and the icons (font icons)
@main-font-color: #CCCCCC; // e.g. rgb(255, 255, 255) or #ffffff
// The default color of control backgrounds is mostly black but with a little
// bit of blue so it can still be seen on all black video frames, which are
// The default color of control backgrounds is mostly black but with a little
// bit of blue so it can still be seen on all black video frames, which are
// common.
@control-bg-color: #07141E; // e.g. rgb(255, 255, 255) or #ffffff
@control-bg-alpha: 0.7; // 1.0 = 100% opacity, 0.0 = 0% opacity
@ -32,12 +33,12 @@ Create your own skin at http://designer.videojs.com
@slider-background-color: #333333;
@slider-background-alpha: 0.9; // 1.0 = 100% opacity, 0.0 = 0% opacity
// The "Big Play Button" is the play button that shows before the video plays.
// To center it set the align values to center and middle. The typical location
// The "Big Play Button" is the play button that shows before the video plays.
// To center it set the align values to center and middle. The typical location
// of the button is the center, but there is trend towards moving it to a corner
// where it gets out of the way of valuable content in the poster image.
@big-play-align: left; // left, center, or right
@big-play-vertical-align: top; // top, middle, or bottom
@big-play-vertical-align: top; // top, middle, or bottom
// The button colors match the control colors by default but you can customize
// them by replace the variables (@control-bg-color) with your own color values.
@big-play-bg-color: @control-bg-color;
@ -58,9 +59,9 @@ Create your own skin at http://designer.videojs.com
/* SKIN
================================================================================
The main class name for all skin-specific styles. To make your own skin,
replace all occurances of 'vjs-default-skin' with a new name. Then add your new
skin name to your video tag instead of the default skin.
The main class name for all skin-specific styles. To make your own skin,
replace all occurances of 'vjs-default-skin' with a new name. Then add your new
skin name to your video tag instead of the default skin.
e.g. <video class="video-js my-skin-name">
*/
.vjs-default-skin {
@ -98,7 +99,7 @@ The control icons are from a custom font. Each icon corresponds to a character
@captions-icon: "\e008";
/* Base UI Component Classes
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
*/
/* Slider - used for Volume bar and Seek bar */
@ -141,13 +142,13 @@ The control icons are from a custom font. Each icon corresponds to a character
/* Control Bar
--------------------------------------------------------------------------------
The default control bar that is a container for most of the controls.
The default control bar that is a container for most of the controls.
*/
.vjs-default-skin .vjs-control-bar {
/* Start hidden *///
display: none;
position: absolute;
/* Place control bar at the bottom of the player box/video.
/* Place control bar at the bottom of the player box/video.
If you want more margin below the control bar, add more height. *///
bottom: 0;
/* Use left/right to stretch to 100% width of player div *///
@ -159,6 +160,36 @@ The default control bar that is a container for most of the controls.
.background-color-with-alpha(@control-bg-color, @control-bg-alpha);
}
/* Show the control bar only once the video has started playing */
.vjs-default-skin.vjs-has-started .vjs-control-bar {
display: block;
/* Visibility needed to make sure things hide in older browsers too. */
visibility: visible;
opacity: 1;
@trans: visibility 0.1s, opacity 0.1s; // Var needed because of comma
.transition(@trans);
}
/* Hide the control bar when the video is playing and the user is inactive */
.vjs-default-skin.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar {
display: block;
visibility: hidden;
opacity: 0;
@trans: visibility 1.0s, opacity 1.0s;
.transition(@trans);
}
.vjs-default-skin.vjs-controls-disabled .vjs-control-bar {
display: none;
}
.vjs-default-skin.vjs-using-native-controls .vjs-control-bar {
display: none;
}
/* General styles for individual controls. *///
.vjs-default-skin .vjs-control {
outline: none;
@ -191,15 +222,15 @@ The default control bar that is a container for most of the controls.
text-shadow: 0em 0em 1em rgba(255, 255, 255, 1);
}
.vjs-default-skin .vjs-control:focus {
.vjs-default-skin .vjs-control:focus {
/* outline: 0; *///
/* keyboard-only users cannot see the focus on several of the UI elements when
this is set to 0 */
}
/* Hide control text visually, but have it available for screenreaders */
.vjs-default-skin .vjs-control-text {
.hide-visually;
.vjs-default-skin .vjs-control-text {
.hide-visually;
}
/* Play/Pause
@ -388,7 +419,7 @@ The default control bar that is a container for most of the controls.
/* Big Play Button (play button at start)
--------------------------------------------------------------------------------
Positioning of the play button in the center or other corners can be done more
Positioning of the play button in the center or other corners can be done more
easily in the skin designer. http://designer.videojs.com/
*/
.vjs-default-skin .vjs-big-play-button {
@ -420,6 +451,20 @@ easily in the skin designer. http://designer.videojs.com/
.transition(all 0.4s);
}
/* Hide if controls are disabled */
.vjs-default-skin.vjs-controls-disabled .vjs-big-play-button {
display: none;
}
/* Hide when video starts playing */
.vjs-default-skin.vjs-has-started .vjs-big-play-button {
display: none;
}
/* Hide on mobile devices. Remove when we stop using native controls
by default on mobile */
.vjs-default-skin.vjs-using-native-controls .vjs-big-play-button {
display: none;
}
.vjs-default-skin:hover .vjs-big-play-button,
.vjs-default-skin .vjs-big-play-button:focus {
outline: 0;
@ -609,18 +654,18 @@ REQUIRED STYLES (be careful overriding)
================================================================================
When loading the player, the video tag is replaced with a DIV,
that will hold the video tag or object tag for other playback methods.
The div contains the video playback element (Flash or HTML5) and controls,
The div contains the video playback element (Flash or HTML5) and controls,
and sets the width and height of the video.
** If you want to add some kind of border/padding (e.g. a frame), or special
positioning, use another containing element. Otherwise you risk messing up
** If you want to add some kind of border/padding (e.g. a frame), or special
positioning, use another containing element. Otherwise you risk messing up
control positioning and full window mode. **
*/
.video-js {
background-color: #000;
position: relative;
padding: 0;
/* Start with 10px for base font size so other dimensions can be em based and
/* Start with 10px for base font size so other dimensions can be em based and
easily calculable. */
font-size: @base-font-size;
/* Allow poster to be vertially aligned. */
@ -649,7 +694,7 @@ control positioning and full window mode. **
height: 100%;
}
/* Fix for Firefox 9 fullscreen (only if it is enabled). Not needed when
/* Fix for Firefox 9 fullscreen (only if it is enabled). Not needed when
checking fullScreenEnabled. */
.video-js:-moz-full-screen { position: absolute; }
@ -675,9 +720,12 @@ body.vjs-full-window {
_position: absolute;
}
.video-js:-webkit-full-screen {
width: 100% !important;
width: 100% !important;
height: 100% !important;
}
.video-js.vjs-fullscreen.vjs-user-inactive {
cursor: none;
}
/* Poster Styles */
.vjs-poster {
@ -699,6 +747,11 @@ body.vjs-full-window {
width: 100%;
}
/* Hide the poster when native controls are used otherwise it covers them */
.video-js.vjs-using-native-controls .vjs-poster {
display: none;
}
/* Text Track Styles */
/* Overall track holder for both captions and subtitles */
.video-js .vjs-text-track-display {
@ -722,26 +775,6 @@ body.vjs-full-window {
.video-js .vjs-captions { color: #fc6 /* Captions are yellow */; }
.vjs-tt-cue { display: block; }
/* Fading sytles, used to fade control bar. */
.vjs-fade-in {
display: block !important;
/* Visibility needed to make sure things hide in older browsers too. */
visibility: visible;
opacity: 1;
@trans: visibility 0.1s, opacity 0.1s; // Var needed because of comma
.transition(@trans);
}
.vjs-fade-out {
display: block !important;
visibility: hidden;
opacity: 0;
@trans: visibility 1.5s, opacity 1.5s;
.transition(@trans);
/* Wait a moment before fading out the control bar */
.transition-delay(2s);
}
/* Hide disabled or unsupported controls */
.vjs-default-skin .vjs-hidden { display: none; }
@ -751,9 +784,9 @@ body.vjs-full-window {
visibility: visible;
}
// MIXINS
// MIXINS
// =============================================================================
// Mixins are a LESS feature and are used to add vendor prefixes to CSS rules
// Mixins are a LESS feature and are used to add vendor prefixes to CSS rules
// when needed.
// https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow
@ -813,7 +846,7 @@ body.vjs-full-window {
.user-select (@string: none) {
/* user-select *///
-webkit-user-select: @string;
-moz-user-select: @string;
-moz-user-select: @string;
-ms-user-select: @string;
user-select: @string;
}
@ -822,14 +855,14 @@ body.vjs-full-window {
// http://h5bp.com/v
.hide-visually () {
/* hide-visually *///
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position:
absolute;
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position:
absolute;
width: 1px;
}
@ -882,27 +915,27 @@ body.vjs-full-window {
// * We want this file to continue to be accessible by people who don't know
// LESS but know CSS. This means finding the balance between using the most
// valuable LESS features (e.g. variables) and keeping it looking like CSS.
// So it's best to avoid advanced LESS features like conditional statements.
// (we're using one for the big play button position because that's a hot
// So it's best to avoid advanced LESS features like conditional statements.
// (we're using one for the big play button position because that's a hot
// topic)
//
//
// * We care about the readability of the CSS output of LESS, which means we
// have to be careful about what features of LESS we use. (if you're building
// your own skin this may not apply)
// 1. Comments inside of rules (strangely) have an extra line added after
// them in the CSS output. To avoid this we can add a LESS comment after
// 1. Comments inside of rules (strangely) have an extra line added after
// them in the CSS output. To avoid this we can add a LESS comment after
// the CSS comment.
// /* comment *///
//
//
// 2. In a rule with nested rules, any comments outside of a rule are moved
// to the top of the parent rule. i.e. it might look like:
// /* title of rule 1 */
// /* title of rule 2 */
// .rule1 {}
// .rule2 {}
// This is why we aren't using nested rules inside of the
// This is why we aren't using nested rules inside of the
// vjs-default-skin class.
/* -----------------------------------------------------------------------------
The original source of this file lives at
/* -----------------------------------------------------------------------------
The original source of this file lives at
https://github.com/videojs/video.js/blob/master/src/css/video-js.less */

View File

@ -1,24 +1,13 @@
/* Big Play Button
================================================================================ */
/**
* Initial play button. Shows before the video has played.
* Initial play button. Shows before the video has played. The hiding of the
* big play button is done via CSS and player states.
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
vjs.BigPlayButton = vjs.Button.extend({
/** @constructor */
init: function(player, options){
vjs.Button.call(this, player, options);
if (!player.controls()) {
this.hide();
}
player.on('play', vjs.bind(this, this.hide));
// player.on('ended', vjs.bind(this, this.show));
}
});
vjs.BigPlayButton = vjs.Button.extend();
vjs.BigPlayButton.prototype.createEl = function(){
return vjs.Button.prototype.createEl.call(this, 'div', {
@ -29,10 +18,5 @@ vjs.BigPlayButton.prototype.createEl = function(){
};
vjs.BigPlayButton.prototype.onClick = function(){
// Go back to the beginning if big play button is showing at the end.
// Have to check for current time otherwise it might throw a 'not ready' error.
//if(this.player_.currentTime()) {
//this.player_.currentTime(0);
//}
this.player_.play();
};

View File

@ -12,7 +12,9 @@ vjs.Button = vjs.Component.extend({
vjs.Component.call(this, player, options);
var touchstart = false;
this.on('touchstart', function() {
this.on('touchstart', function(event) {
// Stop click and other mouse events from triggering also
event.preventDefault();
touchstart = true;
});
this.on('touchmove', function() {
@ -24,7 +26,6 @@ vjs.Button = vjs.Component.extend({
self.onClick(event);
}
event.preventDefault();
event.stopPropagation();
});
this.on('click', this.onClick);

View File

@ -539,26 +539,6 @@ vjs.Component.prototype.hide = function(){
return this;
};
/**
* Fade a component in using CSS
* @return {vjs.Component}
*/
vjs.Component.prototype.fadeIn = function(){
this.removeClass('vjs-fade-out');
this.addClass('vjs-fade-in');
return this;
};
/**
* Fade a component out using CSS
* @return {vjs.Component}
*/
vjs.Component.prototype.fadeOut = function(){
this.removeClass('vjs-fade-in');
this.addClass('vjs-fade-out');
return this;
};
/**
* Lock an item in its visible state. To be used with fadeIn/fadeOut.
* @return {vjs.Component}
@ -583,15 +563,8 @@ vjs.Component.prototype.unlockShowing = function(){
vjs.Component.prototype.disable = function(){
this.hide();
this.show = function(){};
this.fadeIn = function(){};
};
// TODO: Get enable working
// vjs.Component.prototype.enable = function(){
// this.fadeIn = vjs.Component.prototype.fadeIn;
// this.show = vjs.Component.prototype.show;
// };
/**
* If a value is provided it will change the width of the player to that value
* otherwise the width is returned
@ -693,3 +666,50 @@ vjs.Component.prototype.dimension = function(widthOrHeight, num, skipListeners){
// }
}
};
/**
* Emit 'tap' events when touch events are supported. We're requireing them to
* be enabled because otherwise every component would have this extra overhead
* unnecessarily, on mobile devices where extra overhead is especially bad.
*
* This is being implemented so we can support taps on the video element
* toggling the controls.
*/
vjs.Component.prototype.emitTapEvents = function(){
var touchStart, touchTime, couldBeTap, noTap;
// Track the start time so we can determine how long the touch lasted
touchStart = 0;
this.on('touchstart', function(event) {
// Record start time so we can detect a tap vs. "touch and hold"
touchStart = new Date().getTime();
// Reset couldBeTap tracking
couldBeTap = true;
});
noTap = function(){
couldBeTap = false;
};
// TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s
this.on('touchmove', noTap);
this.on('touchleave', noTap);
this.on('touchcancel', noTap);
// When the touch ends, measure how long it took and trigger the appropriate
// event
this.on('touchend', function() {
// Proceed only if the touchmove/leave/cancel event didn't happen
if (couldBeTap === true) {
// Measure how long the touch lasted
touchTime = new Date().getTime() - touchStart;
// The touch needs to be quick in order to consider it a tap
if (touchTime < 250) {
this.trigger('tap');
// It may be good to copy the touchend event object and change the
// type to tap, if the other event properties aren't exact after
// vjs.fixEvent runs (e.g. event.target)
}
}
});
};

View File

@ -4,55 +4,7 @@
* @param {Object=} options
* @constructor
*/
vjs.ControlBar = vjs.Component.extend({
/** @constructor */
init: function(player, options){
vjs.Component.call(this, player, options);
if (!player.controls()) {
this.disable();
}
player.one('play', vjs.bind(this, function(){
var touchstart,
fadeIn = vjs.bind(this, this.fadeIn),
fadeOut = vjs.bind(this, this.fadeOut);
this.fadeIn();
if ( !('ontouchstart' in window) ) {
this.player_.on('mouseover', fadeIn);
this.player_.on('mouseout', fadeOut);
this.player_.on('pause', vjs.bind(this, this.lockShowing));
this.player_.on('play', vjs.bind(this, this.unlockShowing));
}
touchstart = false;
this.player_.on('touchstart', function() {
touchstart = true;
});
this.player_.on('touchmove', function() {
touchstart = false;
});
this.player_.on('touchend', vjs.bind(this, function(event) {
var idx;
if (touchstart) {
idx = this.el().className.search('fade-in');
if (idx !== -1) {
this.fadeOut();
} else {
this.fadeIn();
}
}
touchstart = false;
if (!this.player_.paused()) {
event.preventDefault();
}
}));
}));
}
});
vjs.ControlBar = vjs.Component.extend();
vjs.ControlBar.prototype.options_ = {
loadEvent: 'play',
@ -75,13 +27,3 @@ vjs.ControlBar.prototype.createEl = function(){
className: 'vjs-control-bar'
});
};
vjs.ControlBar.prototype.fadeIn = function(){
vjs.Component.prototype.fadeIn.call(this);
this.player_.trigger('controlsvisible');
};
vjs.ControlBar.prototype.fadeOut = function(){
vjs.Component.prototype.fadeOut.call(this);
this.player_.trigger('controlshidden');
};

View File

@ -55,14 +55,13 @@ goog.exportProperty(vjs.Component.prototype, 'width', vjs.Component.prototype.wi
goog.exportProperty(vjs.Component.prototype, 'height', vjs.Component.prototype.height);
goog.exportProperty(vjs.Component.prototype, 'dimensions', vjs.Component.prototype.dimensions);
goog.exportProperty(vjs.Component.prototype, 'ready', vjs.Component.prototype.ready);
goog.exportProperty(vjs.Component.prototype, 'fadeIn', vjs.Component.prototype.fadeIn);
goog.exportProperty(vjs.Component.prototype, 'fadeOut', vjs.Component.prototype.fadeOut);
goog.exportSymbol('videojs.Player', vjs.Player);
goog.exportProperty(vjs.Player.prototype, 'dispose', vjs.Player.prototype.dispose);
goog.exportProperty(vjs.Player.prototype, 'requestFullScreen', vjs.Player.prototype.requestFullScreen);
goog.exportProperty(vjs.Player.prototype, 'cancelFullScreen', vjs.Player.prototype.cancelFullScreen);
goog.exportProperty(vjs.Player.prototype, 'bufferedPercent', vjs.Player.prototype.bufferedPercent);
goog.exportProperty(vjs.Player.prototype, 'usingNativeControls', vjs.Player.prototype.usingNativeControls);
goog.exportSymbol('videojs.MediaLoader', vjs.MediaLoader);
goog.exportSymbol('videojs.TextTrackDisplay', vjs.TextTrackDisplay);

View File

@ -343,6 +343,7 @@ vjs.IS_OLD_ANDROID = vjs.IS_ANDROID && (/webkit/i).test(vjs.USER_AGENT) && vjs.A
vjs.IS_FIREFOX = (/Firefox/i).test(vjs.USER_AGENT);
vjs.IS_CHROME = (/Chrome/i).test(vjs.USER_AGENT);
vjs.TOUCH_ENABLED = ('ontouchstart' in window);
/**
* Get an element's attribute values, as defined on the HTML tag

View File

@ -182,9 +182,6 @@ vjs.Flash = vjs.MediaTechController.extend({
// Update reference to playback technology element
tech.el_ = el;
// Now that the element is ready, make a click on the swf play the video
vjs.on(el, 'click', tech.bind(tech.onClick));
// Make sure swf is actually ready. Sometimes the API isn't actually yet.
vjs.Flash.checkReady(tech);
});
@ -328,9 +325,6 @@ vjs.Flash['onReady'] = function(currSwf){
// Update reference to playback technology element
tech.el_ = el;
// Now that the element is ready, make a click on the swf play the video
tech.on('click', tech.onClick);
vjs.Flash.checkReady(tech);
};

View File

@ -35,6 +35,14 @@ vjs.Html5 = vjs.MediaTechController.extend({
this.el_.src = source.src;
}
// Determine if native controls should be used
// Our goal should be to get the custom controls on mobile solid everywhere
// so we can remove this all together. Right now this will block custom
// controls on touch enabled laptops like the Chrome Pixel
if (vjs.TOUCH_ENABLED && player.options()['nativeControlsForTouch'] !== false) {
this.useNativeControls();
}
// Chrome and Safari both have issues with autoplay.
// In Safari (5.1.1), when we move the video element into the container div, autoplay doesn't work.
// In Chrome (15), if you have autoplay + a poster + no controls, the video gets hidden (but audio plays)
@ -46,10 +54,7 @@ vjs.Html5 = vjs.MediaTechController.extend({
}
});
this.on('click', this.onClick);
this.setupTriggers();
this.triggerReady();
}
});
@ -116,6 +121,37 @@ vjs.Html5.prototype.eventHandler = function(e){
e.stopPropagation();
};
vjs.Html5.prototype.useNativeControls = function(){
var tech, player, controlsOn, controlsOff, cleanUp;
tech = this;
player = this.player();
// If the player controls are enabled turn on the native controls
tech.setControls(player.controls());
// Update the native controls when player controls state is updated
controlsOn = function(){
tech.setControls(true);
};
controlsOff = function(){
tech.setControls(false);
};
player.on('controlsenabled', controlsOn);
player.on('controlsdisabled', controlsOff);
// Clean up when not using native controls anymore
cleanUp = function(){
player.off('controlsenabled', controlsOn);
player.off('controlsdisabled', controlsOff);
};
tech.on('dispose', cleanUp);
player.on('usingcustomcontrols', cleanUp);
// Update the state of the player to using native controls
player.usingNativeControls(true);
};
vjs.Html5.prototype.play = function(){ this.el_.play(); };
vjs.Html5.prototype.pause = function(){ this.el_.pause(); };
@ -179,29 +215,19 @@ vjs.Html5.prototype.currentSrc = function(){ return this.el_.currentSrc; };
vjs.Html5.prototype.preload = function(){ return this.el_.preload; };
vjs.Html5.prototype.setPreload = function(val){ this.el_.preload = val; };
vjs.Html5.prototype.autoplay = function(){ return this.el_.autoplay; };
vjs.Html5.prototype.setAutoplay = function(val){ this.el_.autoplay = val; };
vjs.Html5.prototype.controls = function(){ return this.el_.controls; }
vjs.Html5.prototype.setControls = function(val){ this.el_.controls = !!val; }
vjs.Html5.prototype.loop = function(){ return this.el_.loop; };
vjs.Html5.prototype.setLoop = function(val){ this.el_.loop = val; };
vjs.Html5.prototype.error = function(){ return this.el_.error; };
// networkState: function(){ return this.el_.networkState; },
// readyState: function(){ return this.el_.readyState; },
vjs.Html5.prototype.seeking = function(){ return this.el_.seeking; };
// initialTime: function(){ return this.el_.initialTime; },
// startOffsetTime: function(){ return this.el_.startOffsetTime; },
// played: function(){ return this.el_.played; },
// seekable: function(){ return this.el_.seekable; },
vjs.Html5.prototype.ended = function(){ return this.el_.ended; };
// videoTracks: function(){ return this.el_.videoTracks; },
// audioTracks: function(){ return this.el_.audioTracks; },
// videoWidth: function(){ return this.el_.videoWidth; },
// videoHeight: function(){ return this.el_.videoHeight; },
// textTracks: function(){ return this.el_.textTracks; },
// defaultPlaybackRate: function(){ return this.el_.defaultPlaybackRate; },
// playbackRate: function(){ return this.el_.playbackRate; },
// mediaGroup: function(){ return this.el_.mediaGroup; },
// controller: function(){ return this.el_.controller; },
vjs.Html5.prototype.defaultMuted = function(){ return this.el_.defaultMuted; };
/* HTML5 Support Testing ---------------------------------------------------- */

View File

@ -1,5 +1,6 @@
/**
* @fileoverview Media Technology Controller - Base class for media playback technology controllers like Flash and HTML5
* @fileoverview Media Technology Controller - Base class for media playback
* technology controllers like Flash and HTML5
*/
/**
@ -13,37 +14,144 @@ vjs.MediaTechController = vjs.Component.extend({
init: function(player, options, ready){
vjs.Component.call(this, player, options, ready);
// Make playback element clickable
// this.addEvent('click', this.proxy(this.onClick));
// player.triggerEvent('techready');
this.initControlsListeners();
}
});
// destroy: function(){},
// createElement: function(){},
/**
* Set up click and touch listeners for the playback element
* On desktops, a click on the video itself will toggle playback,
* on a mobile device a click on the video toggles controls.
* (toggling controls is done by toggling the user state between active and
* inactive)
*
* A tap can signal that a user has become active, or has become inactive
* e.g. a quick tap on an iPhone movie should reveal the controls. Another
* quick tap should hide them again (signaling the user is in an inactive
* viewing state)
*
* In addition to this, we still want the user to be considered inactive after
* a few seconds of inactivity.
*
* Note: the only part of iOS interaction we can't mimic with this setup
* is a touch and hold on the video element counting as activity in order to
* keep the controls showing, but that shouldn't be an issue. A touch and hold on
* any controls will still keep the user active
*/
vjs.MediaTechController.prototype.initControlsListeners = function(){
var player, tech, activateControls, deactivateControls;
tech = this;
player = this.player();
var activateControls = function(){
if (player.controls() && !player.usingNativeControls()) {
tech.addControlsListeners();
}
};
deactivateControls = vjs.bind(tech, tech.removeControlsListeners);
// Set up event listeners once the tech is ready and has an element to apply
// listeners to
this.ready(activateControls);
player.on('controlsenabled', activateControls);
player.on('controlsdisabled', deactivateControls);
};
vjs.MediaTechController.prototype.addControlsListeners = function(){
var preventBubble, userWasActive;
// Some browsers (Chrome & IE) don't trigger a click on a flash swf, but do
// trigger mousedown/up.
// http://stackoverflow.com/questions/1444562/javascript-onclick-event-over-flash-object
// Any touch events are set to block the mousedown event from happening
this.on('mousedown', this.onClick);
// We need to block touch events on the video element from bubbling up,
// otherwise they'll signal activity prematurely. The specific use case is
// when the video is playing and the controls have faded out. In this case
// only a tap (fast touch) should toggle the user active state and turn the
// controls back on. A touch and move or touch and hold should not trigger
// the controls (per iOS as an example at least)
//
// We always want to stop propagation on touchstart because touchstart
// at the player level starts the touchInProgress interval. We can still
// report activity on the other events, but won't let them bubble for
// consistency. We don't want to bubble a touchend without a touchstart.
this.on('touchstart', function(event) {
// Stop the mouse events from also happening
event.preventDefault();
event.stopPropagation();
// Record if the user was active now so we don't have to keep polling it
userWasActive = this.player_.userActive();
});
preventBubble = function(event){
event.stopPropagation();
if (userWasActive) {
this.player_.reportUserActivity();
}
};
// Treat all touch events the same for consistency
this.on('touchmove', preventBubble);
this.on('touchleave', preventBubble);
this.on('touchcancel', preventBubble);
this.on('touchend', preventBubble);
// Turn on component tap events
this.emitTapEvents();
// The tap listener needs to come after the touchend listener because the tap
// listener cancels out any reportedUserActivity when setting userActive(false)
this.on('tap', this.onTap);
};
/**
* Handle a click on the media element. By default will play the media.
*
* On android browsers, having this toggle play state interferes with being
* able to toggle the controls and toggling play state with the play button
* Remove the listeners used for click and tap controls. This is needed for
* toggling to controls disabled, where a tap/touch should do nothing.
*/
vjs.MediaTechController.prototype.onClick = (function(){
if (vjs.IS_ANDROID) {
return function () {};
} else {
return function () {
if (this.player_.controls()) {
if (this.player_.paused()) {
this.player_.play();
} else {
this.player_.pause();
}
}
};
vjs.MediaTechController.prototype.removeControlsListeners = function(){
// We don't want to just use `this.off()` because there might be other needed
// listeners added by techs that extend this.
this.off('tap');
this.off('touchstart');
this.off('touchmove');
this.off('touchleave');
this.off('touchcancel');
this.off('touchend');
this.off('click');
this.off('mousedown');
};
/**
* Handle a click on the media element. By default will play/pause the media.
*/
vjs.MediaTechController.prototype.onClick = function(event){
// We're using mousedown to detect clicks thanks to Flash, but mousedown
// will also be triggered with right-clicks, so we need to prevent that
if (event.button !== 0) return;
// When controls are disabled a click should not toggle playback because
// the click is considered a control
if (this.player().controls()) {
if (this.player().paused()) {
this.player().play();
} else {
this.player().pause();
}
}
})();
};
/**
* Handle a tap on the media element. By default it will toggle the user
* activity state, which hides and shows the controls.
*/
vjs.MediaTechController.prototype.onTap = function(){
this.player().userActive(!this.player().userActive());
};
vjs.MediaTechController.prototype.features = {
volumeControl: true,

View File

@ -24,22 +24,30 @@ vjs.Player = vjs.Component.extend({
this.poster_ = options['poster'];
// Set controls
this.controls_ = options['controls'];
// Use native controls for iOS and Android by default
// until controls are more stable on those devices.
if (options['customControlsOnMobile'] !== true && (vjs.IS_IOS || vjs.IS_ANDROID)) {
tag.controls = options['controls'];
this.controls_ = false;
} else {
// Original tag settings stored in options
// now remove immediately so native controls don't flash.
tag.controls = false;
}
// Original tag settings stored in options
// now remove immediately so native controls don't flash.
// May be turned back on by HTML5 tech if nativeControlsForTouch is true
tag.controls = false;
// Run base component initializing with new options.
// Builds the element through createEl()
// Inits and embeds any child components in opts
vjs.Component.call(this, this, options, ready);
// Update controls className. Can't do this when the controls are initially
// set because the element doesn't exist yet.
if (this.controls()) {
this.addClass('vjs-controls-enabled');
} else {
this.addClass('vjs-controls-disabled');
}
// TODO: Make this smarter. Toggle user state between touching/mousing
// using events, since devices can have both touch and mouse events.
// if (vjs.TOUCH_ENABLED) {
// this.addClass('vjs-touch-enabled');
// }
// Firstplay event implimentation. Not sold on the event yet.
// Could probably just check currentTime==0?
this.one('play', function(e){
@ -71,6 +79,8 @@ vjs.Player = vjs.Component.extend({
this[key](val);
}, this);
}
this.listenForUserActivity();
}
});
@ -377,6 +387,8 @@ vjs.Player.prototype.onFirstPlay = function(){
if(this.options_['starttime']){
this.currentTime(this.options_['starttime']);
}
this.addClass('vjs-has-started');
};
vjs.Player.prototype.onPause = function(){
@ -848,20 +860,188 @@ vjs.Player.prototype.controls_;
* @param {Boolean} controls Set controls to showing or not
* @return {Boolean} Controls are showing
*/
vjs.Player.prototype.controls = function(controls){
if (controls !== undefined) {
vjs.Player.prototype.controls = function(bool){
if (bool !== undefined) {
bool = !!bool; // force boolean
// Don't trigger a change event unless it actually changed
if (this.controls_ !== controls) {
this.controls_ = !!controls; // force boolean
this.trigger('controlschange');
if (this.controls_ !== bool) {
this.controls_ = bool;
if (bool) {
this.removeClass('vjs-controls-disabled');
this.addClass('vjs-controls-enabled');
this.trigger('controlsenabled');
} else {
this.removeClass('vjs-controls-enabled');
this.addClass('vjs-controls-disabled');
this.trigger('controlsdisabled');
}
}
return this;
}
return this.controls_;
};
vjs.Player.prototype.usingNativeControls_;
/**
* Toggle native controls on/off. Native controls are the controls built into
* devices (e.g. default iPhone controls), Flash, or other techs
* (e.g. Vimeo Controls)
*
* **This should only be set by the current tech, because only the tech knows
* if it can support native controls**
*
* @param {Boolean} bool True signals that native controls are on
* @return {vjs.Player} Returns the player
*/
vjs.Player.prototype.usingNativeControls = function(bool){
if (bool !== undefined) {
bool = !!bool; // force boolean
// Don't trigger a change event unless it actually changed
if (this.usingNativeControls_ !== bool) {
this.usingNativeControls_ = bool;
if (bool) {
this.addClass('vjs-using-native-controls');
this.trigger('usingnativecontrols');
} else {
this.removeClass('vjs-using-native-controls');
this.trigger('usingcustomcontrols');
}
}
return this;
}
return this.usingNativeControls_;
};
vjs.Player.prototype.error = function(){ return this.techGet('error'); };
vjs.Player.prototype.ended = function(){ return this.techGet('ended'); };
// When the player is first initialized, trigger activity so components
// like the control bar show themselves if needed
vjs.Player.prototype.userActivity_ = true;
vjs.Player.prototype.reportUserActivity = function(event){
this.userActivity_ = true;
};
vjs.Player.prototype.userActive_ = true;
vjs.Player.prototype.userActive = function(bool){
if (bool !== undefined) {
bool = !!bool;
if (bool !== this.userActive_) {
this.userActive_ = bool;
if (bool) {
// If the user was inactive and is now active we want to reset the
// inactivity timer
this.userActivity_ = true;
this.removeClass('vjs-user-inactive');
this.addClass('vjs-user-active');
this.trigger('useractive');
} else {
// We're switching the state to inactive manually, so erase any other
// activity
this.userActivity_ = false;
// Chrome/Safari/IE have bugs where when you change the cursor it can
// trigger a mousemove event. This causes an issue when you're hiding
// the cursor when the user is inactive, and a mousemove signals user
// activity. Making it impossible to go into inactive mode. Specifically
// this happens in fullscreen when we really need to hide the cursor.
//
// When this gets resolved in ALL browsers it can be removed
// https://code.google.com/p/chromium/issues/detail?id=103041
this.tech.one('mousemove', function(e){
e.stopPropagation();
e.preventDefault();
});
this.removeClass('vjs-user-active');
this.addClass('vjs-user-inactive');
this.trigger('userinactive');
}
}
return this;
}
return this.userActive_;
};
vjs.Player.prototype.listenForUserActivity = function(){
var onMouseActivity, onMouseDown, mouseInProgress, onMouseUp,
activityCheck, inactivityTimeout;
onMouseActivity = this.reportUserActivity;
onMouseDown = function() {
onMouseActivity();
// For as long as the they are touching the device or have their mouse down,
// we consider them active even if they're not moving their finger or mouse.
// So we want to continue to update that they are active
clearInterval(mouseInProgress);
// Setting userActivity=true now and setting the interval to the same time
// as the activityCheck interval (250) should ensure we never miss the
// next activityCheck
mouseInProgress = setInterval(vjs.bind(this, onMouseActivity), 250);
};
onMouseUp = function(event) {
onMouseActivity();
// Stop the interval that maintains activity if the mouse/touch is down
clearInterval(mouseInProgress);
};
// Any mouse movement will be considered user activity
this.on('mousedown', onMouseDown);
this.on('mousemove', onMouseActivity);
this.on('mouseup', onMouseUp);
// Listen for keyboard navigation
// Shouldn't need to use inProgress interval because of key repeat
this.on('keydown', onMouseActivity);
this.on('keyup', onMouseActivity);
// Consider any touch events that bubble up to be activity
// Certain touches on the tech will be blocked from bubbling because they
// toggle controls
this.on('touchstart', onMouseDown);
this.on('touchmove', onMouseActivity);
this.on('touchend', onMouseUp);
this.on('touchcancel', onMouseUp);
// Run an interval every 250 milliseconds instead of stuffing everything into
// the mousemove/touchmove function itself, to prevent performance degradation.
// `this.reportUserActivity` simply sets this.userActivity_ to true, which
// then gets picked up by this loop
// http://ejohn.org/blog/learning-from-twitter/
activityCheck = setInterval(vjs.bind(this, function() {
// Check to see if mouse/touch activity has happened
if (this.userActivity_) {
// Reset the activity tracker
this.userActivity_ = false;
// If the user state was inactive, set the state to active
this.userActive(true);
// Clear any existing inactivity timeout to start the timer over
clearTimeout(inactivityTimeout);
// In X seconds, if no more activity has occurred the user will be
// considered inactive
inactivityTimeout = setTimeout(vjs.bind(this, function() {
// Protect against the case where the inactivityTimeout can trigger just
// before the next user activity is picked up by the activityCheck loop
// causing a flicker
if (!this.userActivity_) {
this.userActive(false);
}
}), 2000);
}
}), 250);
// Clean up the intervals when we kill the player
this.on('dispose', function(){
clearInterval(activityCheck);
clearTimeout(inactivityTimeout);
});
};
// Methods to add support for
// networkState: function(){ return this.techCall('networkState'); },
// readyState: function(){ return this.techCall('readyState'); },
@ -929,3 +1109,5 @@ vjs.Player.prototype.ended = function(){ return this.techGet('ended'); };
}
})();

View File

@ -40,5 +40,8 @@ vjs.PosterImage.prototype.createEl = function(){
};
vjs.PosterImage.prototype.onClick = function(){
this.player_.play();
// Only accept clicks when controls are enabled
if (this.player().controls()) {
this.player_.play();
}
};

View File

@ -25,6 +25,7 @@ test('should be able to access expected player API methods', function() {
ok(player.one, 'one exists');
ok(player.bufferedPercent, 'bufferedPercent exists');
ok(player.dimensions, 'dimensions exists');
ok(player.usingNativeControls, 'usingNativeControls exists');
player.dispose();
});
@ -66,7 +67,7 @@ test('videojs.players should be availble after minification', function() {
fixture.appendChild(videoTag);
var player = videojs(id);
equal(videojs.players[id], player, 'videojs.players is available');
ok(videojs.players[id] === player, 'videojs.players is available');
player.dispose();
});

View File

@ -221,3 +221,29 @@ test('should use a defined content el for appending children', function(){
ok(comp.el().childNodes[0]['id'] === 'contentEl', 'Content El should still exist');
ok(comp.el().childNodes[0].childNodes[0] !== child.el(), 'Child el should be removed.');
});
test('should emit a tap event', function(){
expect(1);
// Fake touch support. Real touch support isn't needed for this test.
var origTouch = vjs.TOUCH_ENABLED;
vjs.TOUCH_ENABLED = true;
var comp = new vjs.Component(getFakePlayer(), {});
comp.emitTapEvents();
comp.on('tap', function(){
ok(true, 'Tap event emitted');
});
comp.trigger('touchstart');
comp.trigger('touchend');
// This second test should not trigger another tap event because
// a touchmove is happening
comp.trigger('touchstart');
comp.trigger('touchmove');
comp.trigger('touchend');
// Reset to orignial value
vjs.TOUCH_ENABLED = origTouch;
});

0
test/unit/control-bar.js Normal file
View File

View File

@ -21,10 +21,15 @@ test('should re-link the player if the tech is moved', function(){
var player, tech, el;
el = document.createElement('div');
el.innerHTML = '<div />';
player = {
id: function(){ return 'id'; },
el: function(){ return el; },
options_: {},
options: function(){ return {}; },
controls: function(){ return false; },
usingNativeControls: function(){ return false; },
on: function(){ return this; },
ready: function(){}
};
tech = new vjs.Html5(player, {});
@ -32,4 +37,4 @@ test('should re-link the player if the tech is moved', function(){
tech.createEl();
strictEqual(player, tech.el()['player']);
});
});

View File

@ -211,20 +211,29 @@ test('should be able to initialize player twice on the same tag using string ref
player.dispose();
});
test('should set controls and trigger event', function() {
expect(3);
test('should set controls and trigger events', function() {
expect(6);
var player = PlayerTest.makePlayer({ 'controls': false });
ok(player.controls() === false, 'controls set through options');
var hasDisabledClass = player.el().className.indexOf('vjs-controls-disabled');
ok(hasDisabledClass !== -1, 'Disabled class added to player');
player.controls(true);
ok(player.controls() === true, 'controls updated');
var hasEnabledClass = player.el().className.indexOf('vjs-controls-enabled');
ok(hasEnabledClass !== -1, 'Disabled class added to player');
player.on('controlschange', function(){
ok(true, 'controlschange fired once');
player.on('controlsenabled', function(){
ok(true, 'enabled fired once');
});
player.on('controlsdisabled', function(){
ok(true, 'disabled fired once');
});
player.controls(false);
// Check for unnecessary controlschange events
player.controls(false);
player.controls(true);
// Check for unnecessary events
player.controls(true);
player.dispose();
});
@ -247,3 +256,76 @@ test('should set controls and trigger event', function() {
// player.requestFullScreen();
// });
test('should toggle user the user state between active and inactive', function(){
var player = PlayerTest.makePlayer({});
expect(9);
ok(player.userActive(), 'User should be active at player init');
player.on('userinactive', function(){
ok(true, 'userinactive event triggered');
});
player.on('useractive', function(){
ok(true, 'useractive event triggered');
});
player.userActive(false);
ok(player.userActive() === false, 'Player state changed to inactive');
ok(player.el().className.indexOf('vjs-user-active') === -1, 'Active class removed');
ok(player.el().className.indexOf('vjs-user-inactive') !== -1, 'Inactive class added');
player.userActive(true);
ok(player.userActive() === true, 'Player state changed to active');
ok(player.el().className.indexOf('vjs-user-inactive') === -1, 'Inactive class removed');
ok(player.el().className.indexOf('vjs-user-active') !== -1, 'Active class added');
player.dispose();
});
test('should add a touch-enabled classname when touch is supported', function(){
var player;
expect(1);
// Fake touch support. Real touch support isn't needed for this test.
var origTouch = vjs.TOUCH_ENABLED;
vjs.TOUCH_ENABLED = true;
player = PlayerTest.makePlayer({});
ok(player.el().className.indexOf('vjs-touch-enabled'), 'touch-enabled classname added');
vjs.TOUCH_ENABLED = origTouch;
player.dispose();
});
test('should allow for tracking when native controls are used', function(){
var player = PlayerTest.makePlayer({});
expect(6);
// Make sure native controls is false before starting test
player.usingNativeControls(false);
player.on('usingnativecontrols', function(){
ok(true, 'usingnativecontrols event triggered');
});
player.on('usingcustomcontrols', function(){
ok(true, 'usingcustomcontrols event triggered');
});
player.usingNativeControls(true);
ok(player.usingNativeControls() === true, 'Using native controls is true');
ok(player.el().className.indexOf('vjs-using-native-controls') !== -1, 'Native controls class added');
player.usingNativeControls(false);
ok(player.usingNativeControls() === false, 'Using native controls is false');
ok(player.el().className.indexOf('vjs-using-native-controls') === -1, 'Native controls class removed');
player.dispose();
});