Skip to content

Commit

Permalink
Added support for playbackRate switching. closes #1132
Browse files Browse the repository at this point in the history
  • Loading branch information
H1D authored and heff committed May 13, 2014
1 parent 047bd8f commit 8dfe0a4
Show file tree
Hide file tree
Showing 15 changed files with 229 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ CHANGELOG
* Fixed compilation failures with LESS v1.7.0 and GRUNT v0.4.4 [[view](https://github.com/videojs/video.js/pull/1180)]
* Added better error handling across the library [[view](https://github.com/videojs/video.js/pull/1197)]
* Updated captions/subtiles file fetching to support cross-origin requests in older IE browsers [[view](https://github.com/videojs/video.js/pull/1095)]
* Added support for playback rate switching [[view](https://github.com/videojs/video.js/pull/1132)]

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

Expand Down
1 change: 1 addition & 0 deletions build/source-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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
2 changes: 1 addition & 1 deletion sandbox/index.html.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<source src="http://video-js.zencoder.com/oceans-clip.webm" type='video/webm'>
<source src="http://video-js.zencoder.com/oceans-clip.ogv" type='video/ogg'>
<track kind="captions" src="../build/demo-files/demo.captions.vtt" srclang="en" label="English"></track><!-- Tracks need an ending tag thanks to IE9 -->
<p class="vjs-no-js">To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a>
<p class="vjs-no-js">To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a></p>
</video>

<script>
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 @@ -265,6 +265,27 @@ 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);
}

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

/* Volume/Mute
-------------------------------------------------------------------------------- */
.vjs-default-skin .vjs-mute-control,
Expand Down
5 changes: 3 additions & 2 deletions src/js/control-bar/control-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ vjs.ControlBar.prototype.options_ = {
'progressControl': {},
'fullscreenToggle': {},
'volumeControl': {},
'muteToggle': {}
// 'volumeMenuButton': {}
'muteToggle': {},
// 'volumeMenuButton': {},
'playbackRateMenuButton': {}
}
};

Expand Down
129 changes: 129 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,129 @@
/**
* 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',
innerHTML: '<div class="vjs-control-content"><span class="vjs-control-text">Playback Rate</span></div>'
});

this.labelEl_ = vjs.createEl('div', {
className: 'vjs-playback-rate-value',
innerHTML: 1.0
});

el.appendChild(this.labelEl_);

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 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.labelEl_.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);
};
5 changes: 5 additions & 0 deletions src/js/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ vjs.options = {
// defaultVolume: 0.85,
'defaultVolume': 0.00, // The freakin seaguls are driving me crazy!

// default playback rates
'playbackRates': [],
// Add playback rate selection by adding 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 @@ -102,6 +102,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 @@ -136,6 +137,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 @@ -242,6 +245,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 @@ -274,6 +280,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
15 changes: 14 additions & 1 deletion src/js/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -1449,6 +1449,20 @@ vjs.Player.prototype.listenForUserActivity = function(){
});
};

vjs.Player.prototype.playbackRate = function(rate) {
if (rate !== undefined) {
this.techCall('setPlaybackRate', rate);
return this;
}

if (this.tech && this.tech.features && this.tech.features['playbackRate']) {
return this.techGet('playbackRate');
} else {
return 1.0;
}

};

// Methods to add support for
// networkState: function(){ return this.techCall('networkState'); },
// readyState: function(){ return this.techCall('readyState'); },
Expand All @@ -1461,7 +1475,6 @@ vjs.Player.prototype.listenForUserActivity = function(){
// videoWidth: function(){ return this.techCall('videoWidth'); },
// videoHeight: function(){ return this.techCall('videoHeight'); },
// defaultPlaybackRate: function(){ return this.techCall('defaultPlaybackRate'); },
// playbackRate: function(){ return this.techCall('playbackRate'); },
// mediaGroup: function(){ return this.techCall('mediaGroup'); },
// controller: function(){ return this.techCall('controller'); },
// defaultMuted: function(){ return this.techCall('defaultMuted'); }
Expand Down
2 changes: 2 additions & 0 deletions test/unit/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ test('should be able to access expected player API methods', function() {
ok(player.textTracks, 'textTracks exists');
ok(player.requestFullScreen, 'requestFullScreen exists');
ok(player.cancelFullScreen, 'cancelFullScreen exists');
ok(player.playbackRate, 'playbackRate exists');

// Unsupported Native HTML5 Methods
// ok(player.canPlayType, 'canPlayType exists');
Expand Down Expand Up @@ -138,6 +139,7 @@ test('should export useful components to the public', function () {
ok(videojs.Menu, 'Menu should be public');
ok(videojs.MenuItem, 'MenuItem should be public');
ok(videojs.MenuButton, 'MenuButton should be public');
ok(videojs.PlaybackRateMenuButton, 'PlaybackRateMenuButton should be public');

ok(videojs.util, 'util namespace should be public');
ok(videojs.util.mergeOptions, 'mergeOptions should be public');
Expand Down
9 changes: 9 additions & 0 deletions test/unit/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,12 @@ test('calculateDistance should use changedTouches, if available', function() {

equal(slider.calculateDistance(event), 0.5, 'we should have touched exactly in the center, so, the ratio should be half');
});

test('should hide playback rate control if it\'s not supported', function(){
expect(1);

var player = PlayerTest.makePlayer();
var playbackRate = new vjs.PlaybackRateMenuButton(player);

ok(playbackRate.el().className.indexOf('vjs-hidden') >= 0, 'playbackRate is not hidden');
});
27 changes: 27 additions & 0 deletions test/unit/media.html5.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,30 @@ test('should return a maybe for mp4 on OLD ANDROID', function() {
vjs.IS_OLD_ANDROID = isOldAndroid;
vjs.Html5.unpatchCanPlayType();
});

test('test playbackRate', function() {
var el, player, playbackRate, tech;

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, {});
tech.createEl();

tech.el_.playbackRate = 1.25;
strictEqual(tech.playbackRate(), 1.25);

tech['setPlaybackRate'](0.75);
strictEqual(tech.playbackRate(), 0.75);
});

0 comments on commit 8dfe0a4

Please sign in to comment.