Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

playbackRate support #1132

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ CHANGELOG
* Fixed error due to undefined tech when no source is supported [[view](https://github.com/videojs/video.js/pull/1172)]
* Fixed the progress bar not finishing when manual timeupdate events are used [[view](https://github.com/videojs/video.js/pull/1173)]
* Added a more informative and styled fallback message for non-html5 browsers [[view](https://github.com/videojs/video.js/pull/1181)]
* Added the option to provide an array of child components instead of an object [[view](https://github.com/videojs/video.js/pull/1093)]
* Fixed casing on webkitRequestFullscreen [[view](https://github.com/videojs/video.js/pull/1101)]
* Made tap events on mobile less sensitive to touch moves [[view](https://github.com/videojs/video.js/pull/1111)]

--------------------

Expand Down
1 change: 1 addition & 0 deletions build/source-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var sourceFiles = [
"src/js/control-bar/volume-control.js",
"src/js/control-bar/mute-toggle.js",
"src/js/control-bar/volume-menu-button.js",
"src/js/control-bar/playback-rate-menu-button.js",
"src/js/poster.js",
"src/js/loading-spinner.js",
"src/js/big-play-button.js",
Expand Down
21 changes: 21 additions & 0 deletions src/css/video-js.less
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,21 @@ fonts to show/hide properly.
content: @pause-icon;
}

/* Playback toggle
--------------------------------------------------------------------------------
*/
.vjs-default-skin .vjs-playback-rate .vjs-playback-rate-value {
font-size: 1.5em;
line-height: 2;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: center;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}

/* Volume/Mute
-------------------------------------------------------------------------------- */
.vjs-default-skin .vjs-mute-control,
Expand Down Expand Up @@ -636,6 +651,12 @@ easily in the skin designer. http://designer.videojs.com/
.box-shadow(-0.2em -0.2em 0.3em rgba(255, 255, 255, 0.2));
}

.vjs-default-skin .vjs-playback-rate .vjs-menu .vjs-menu-content {
width: 4em;
left: -2em;
list-style: none;
}

.vjs-default-skin .vjs-menu-button:hover .vjs-menu {
display: block;
}
Expand Down
18 changes: 2 additions & 16 deletions src/js/button.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,9 @@ vjs.Button = vjs.Component.extend({
init: function(player, options){
vjs.Component.call(this, player, options);

var touchstart = false;
this.on('touchstart', function(event) {
// Stop click and other mouse events from triggering also
event.preventDefault();
touchstart = true;
});
this.on('touchmove', function() {
touchstart = false;
});
var self = this;
this.on('touchend', function(event) {
if (touchstart) {
self.onClick(event);
}
event.preventDefault();
});
this.emitTapEvents();

this.on('tap', this.onClick);
this.on('click', this.onClick);
this.on('focus', this.onFocus);
this.on('blur', this.onBlur);
Expand Down
38 changes: 32 additions & 6 deletions src/js/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -864,35 +864,61 @@ vjs.Component.prototype.onResize;
* @private
*/
vjs.Component.prototype.emitTapEvents = function(){
var touchStart, touchTime, couldBeTap, noTap;
var touchStart, firstTouch, touchTime, couldBeTap, noTap,
xdiff, ydiff, touchDistance, tapMovementThreshold;

// Track the start time so we can determine how long the touch lasted
touchStart = 0;
firstTouch = null;

// Maximum movement allowed during a touch event to still be considered a tap
tapMovementThreshold = 22;

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;
// If more than one finger, don't consider treating this as a click
if (event.touches.length === 1) {
firstTouch = event.touches[0];
// Record start time so we can detect a tap vs. "touch and hold"
touchStart = new Date().getTime();
// Reset couldBeTap tracking
couldBeTap = true;
}
});

this.on('touchmove', function(event) {
// If more than one finger, don't consider treating this as a click
if (event.touches.length > 1) {
couldBeTap = false;
} else if (firstTouch) {
// Some devices will throw touchmoves for all but the slightest of taps.
// So, if we moved only a small distance, this could still be a tap
xdiff = event.touches[0].pageX - firstTouch.pageX;
ydiff = event.touches[0].pageY - firstTouch.pageY;
touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
if (touchDistance > tapMovementThreshold) {
couldBeTap = false;
}
}
});

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(event) {
firstTouch = null;
// 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) {
event.preventDefault(); // Don't let browser turn this into a click
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
Expand Down
3 changes: 2 additions & 1 deletion src/js/control-bar/control-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ vjs.ControlBar.prototype.options_ = {
'progressControl': {},
'fullscreenToggle': {},
'volumeControl': {},
'muteToggle': {}
'muteToggle': {},
'playbackRateMenuButton': {}
// 'volumeMenuButton': {}
}
};
Expand Down
128 changes: 128 additions & 0 deletions src/js/control-bar/playback-rate-menu-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* The component for controlling the playback rate
*
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
vjs.PlaybackRateMenuButton = vjs.MenuButton.extend({
/** @constructor */
init: function(player, options){
vjs.MenuButton.call(this, player, options);

this.updateVisibility();
this.updateLabel();

player.on('loadstart', vjs.bind(this, this.updateVisibility));
player.on('ratechange', vjs.bind(this, this.updateLabel));
}
});

vjs.PlaybackRateMenuButton.prototype.createEl = function(){
var el = vjs.Component.prototype.createEl.call(this, 'div', {
className: 'vjs-playback-rate vjs-menu-button vjs-control'
});

this.contentEl_ = vjs.createEl('div', {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought using the component's build in contentEl would make sense for this, but it looks like MenuButton already makes use of contentEl. You might be able to just change that to 'labelEl' or something like that.

className: 'vjs-playback-rate-value',
innerHTML: 1.0
});
el.appendChild(this.contentEl_);

return el;
};

// Menu creation
vjs.PlaybackRateMenuButton.prototype.createMenu = function(){
var menu = new vjs.Menu(this.player());
var rates = this.player().options().playbackRates;

if (rates) {
for (var i = rates.length - 1; i >= 0; i--) {
menu.addChild(
new vjs.PlaybackRateMenuItem(this.player(), {rate: rates[i] + 'x'})
);
};
}

return menu;
};

vjs.PlaybackRateMenuButton.prototype.updateARIAAttributes = function(){
// Current playback rate
this.el().setAttribute('aria-valuenow', this.player().playbackRate());
};

vjs.PlaybackRateMenuButton.prototype.onClick = function(){
// select next rate option
var currentRate = this.player().playbackRate();
var rates = this.player().options().playbackRates;
// this will select first one if the last one currently selected
var newRate = rates[0];
for (var i = 0; i <rates.length ; i++) {
if (rates[i] > currentRate) {
newRate = rates[i];
break;
}
};
this.player().playbackRate(newRate);
};

vjs.PlaybackRateMenuButton.prototype.playbackRateSupported = function(){
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got a little too fancy here. :) Should remove the line break after return.

this.player().tech
&& this.player().tech.features['playbackRate']
&& this.player().options().playbackRates
&& this.player().options().playbackRates.length > 0
;
};

/**
* Hide playback rate controls when they're no playback rate options to select
*/
vjs.PlaybackRateMenuButton.prototype.updateVisibility = function(){
if (this.playbackRateSupported()) {
this.removeClass('vjs-hidden');
} else {
this.addClass('vjs-hidden');
}
};

/**
* Update button label when rate changed
*/
vjs.PlaybackRateMenuButton.prototype.updateLabel = function(){
if (this.playbackRateSupported()) {
this.contentEl().innerHTML = this.player().playbackRate() + 'x';
}
};

/**
* The specific menu item type for selecting a playback rate
*
* @constructor
*/
vjs.PlaybackRateMenuItem = vjs.MenuItem.extend({
contentElType: 'button',
/** @constructor */
init: function(player, options){
var label = this.label = options['rate'];
var rate = this.rate = parseFloat(label, 10);

// Modify options for parent MenuItem class's init.
options['label'] = label;
options['selected'] = rate === 1;
vjs.MenuItem.call(this, player, options);

this.player().on('ratechange', vjs.bind(this, this.update));
}
});

vjs.PlaybackRateMenuItem.prototype.onClick = function(){
vjs.MenuItem.prototype.onClick.call(this);
this.player().playbackRate(this.rate);
};

vjs.PlaybackRateMenuItem.prototype.update = function(){
this.selected(this.player().playbackRate() == this.rate);
};
3 changes: 3 additions & 0 deletions src/js/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ vjs.options = {
// defaultVolume: 0.85,
'defaultVolume': 0.00, // The freakin seaguls are driving me crazy!

// default playback rates
'playbackRates': [0.5, 1, 1.5, 2],

// Included control sets
'children': {
'mediaLoader': {},
Expand Down
3 changes: 3 additions & 0 deletions src/js/exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ goog.exportSymbol('videojs.PosterImage', vjs.PosterImage);
goog.exportSymbol('videojs.Menu', vjs.Menu);
goog.exportSymbol('videojs.MenuItem', vjs.MenuItem);
goog.exportSymbol('videojs.MenuButton', vjs.MenuButton);
goog.exportSymbol('videojs.PlaybackRateMenuButton', vjs.PlaybackRateMenuButton);
goog.exportProperty(vjs.MenuButton.prototype, 'createItems', vjs.MenuButton.prototype.createItems);
goog.exportProperty(vjs.TextTrackButton.prototype, 'createItems', vjs.TextTrackButton.prototype.createItems);
goog.exportProperty(vjs.ChaptersButton.prototype, 'createItems', vjs.ChaptersButton.prototype.createItems);
Expand Down Expand Up @@ -135,6 +136,8 @@ goog.exportProperty(vjs.Html5.prototype, 'setAutoplay', vjs.Html5.prototype.setA
goog.exportProperty(vjs.Html5.prototype, 'setLoop', vjs.Html5.prototype.setLoop);
goog.exportProperty(vjs.Html5.prototype, 'enterFullScreen', vjs.Html5.prototype.enterFullScreen);
goog.exportProperty(vjs.Html5.prototype, 'exitFullScreen', vjs.Html5.prototype.exitFullScreen);
goog.exportProperty(vjs.Html5.prototype, 'playbackRate', vjs.Html5.prototype.playbackRate);
goog.exportProperty(vjs.Html5.prototype, 'setPlaybackRate', vjs.Html5.prototype.setPlaybackRate);

goog.exportSymbol('videojs.Flash', vjs.Flash);
goog.exportProperty(vjs.Flash, 'isSupported', vjs.Flash.isSupported);
Expand Down
1 change: 0 additions & 1 deletion src/js/media/flash.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,6 @@ vjs.Flash.prototype.enterFullScreen = function(){
return false;
};


// Create setters and getters for attributes
var api = vjs.Flash.prototype,
readWrite = 'rtmpConnection,rtmpStream,preload,defaultPlaybackRate,playbackRate,autoplay,loop,mediaGroup,controller,controls,volume,muted,defaultMuted'.split(','),
Expand Down
12 changes: 12 additions & 0 deletions src/js/media/html5.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ vjs.Html5 = vjs.MediaTechController.extend({
// volume cannot be changed from 1 on iOS
this.features['volumeControl'] = vjs.Html5.canControlVolume();

// just in case; or is it excessively...
this.features['playbackRate'] = vjs.Html5.canControlPlaybackRate();

// In iOS, if you move a video element in the DOM, it breaks video playback.
this.features['movingMediaElementInDOM'] = !vjs.IS_IOS;

Expand Down Expand Up @@ -234,6 +237,9 @@ vjs.Html5.prototype.seeking = function(){ return this.el_.seeking; };
vjs.Html5.prototype.ended = function(){ return this.el_.ended; };
vjs.Html5.prototype.defaultMuted = function(){ return this.el_.defaultMuted; };

vjs.Html5.prototype.playbackRate = function(){ return this.el_.playbackRate; };
vjs.Html5.prototype.setPlaybackRate = function(val){ this.el_.playbackRate = val; };

/* HTML5 Support Testing ---------------------------------------------------- */

vjs.Html5.isSupported = function(){
Expand Down Expand Up @@ -266,6 +272,12 @@ vjs.Html5.canControlVolume = function(){
return volume !== vjs.TEST_VID.volume;
};

vjs.Html5.canControlPlaybackRate = function(){
var playbackRate = vjs.TEST_VID.playbackRate;
vjs.TEST_VID.playbackRate = (playbackRate / 2) + 0.1;
return playbackRate !== vjs.TEST_VID.playbackRate;
};

// HTML5 Feature detection and Device Fixes --------------------------------- //
(function() {
var canPlayType,
Expand Down
1 change: 1 addition & 0 deletions src/js/media/media.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ vjs.MediaTechController.prototype.features = {

// Resizing plugins using request fullscreen reloads the plugin
'fullscreenResize': false,
'playbackRate': false,

// Optional events that we can manually mimic with timers
// currently not triggered by video-js-swf
Expand Down
Loading