mirror of
https://github.com/videojs/video.js.git
synced 2024-12-25 02:42:10 +02:00
Added support for playbackRate switching. closes #1132
This commit is contained in:
parent
047bd8f618
commit
8dfe0a4d06
@ -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)]
|
||||
|
||||
--------------------
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -20,8 +20,9 @@ vjs.ControlBar.prototype.options_ = {
|
||||
'progressControl': {},
|
||||
'fullscreenToggle': {},
|
||||
'volumeControl': {},
|
||||
'muteToggle': {}
|
||||
// 'volumeMenuButton': {}
|
||||
'muteToggle': {},
|
||||
// 'volumeMenuButton': {},
|
||||
'playbackRateMenuButton': {}
|
||||
}
|
||||
};
|
||||
|
||||
|
129
src/js/control-bar/playback-rate-menu-button.js
Normal file
129
src/js/control-bar/playback-rate-menu-button.js
Normal file
@ -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);
|
||||
};
|
@ -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': {},
|
||||
|
@ -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);
|
||||
@ -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);
|
||||
|
@ -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(','),
|
||||
|
@ -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;
|
||||
|
||||
@ -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(){
|
||||
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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'); },
|
||||
@ -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'); }
|
||||
|
@ -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');
|
||||
@ -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');
|
||||
|
9
test/unit/controls.js
vendored
9
test/unit/controls.js
vendored
@ -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');
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user