Merged master into github/dev

This commit is contained in:
Kasper Moskwiak 2016-09-13 23:14:01 +02:00
commit 781ca81d18
11 changed files with 661 additions and 245 deletions

View file

@ -3,4 +3,6 @@ node_js:
- 4 - 4
- 0.12 - 0.12
- 0.10 - 0.10
before_install: npm install -g grunt-cli
install: npm install
sudo: false sudo: false

View file

@ -81,4 +81,8 @@ module.exports = function(grunt) {
'qunit', 'qunit',
'concat', 'concat',
'uglify']); 'uglify']);
grunt.registerTask('test', [
'jshint'
]);
}; };

View file

@ -1,7 +1,11 @@
# Video.js Resolution Switcher # Video.js Resolution Switcher [![Build Status](https://travis-ci.org/kmoskwiak/videojs-resolution-switcher.svg?branch=master)](https://travis-ci.org/kmoskwiak/videojs-resolution-switcher)
Resolution switcher for [video.js v5](https://github.com/videojs/video.js) Resolution switcher for [video.js v5](https://github.com/videojs/video.js)
## Example
[Working examples](examples) of the plugin you can check out if you're having trouble. Or check out this [demo](https://kmoskwiak.github.io/videojs-resolution-switcher/).
## Getting Started ## Getting Started
Install plugin with Install plugin with
@ -70,6 +74,36 @@ bower install videojs-resolution-switcher
``` ```
### YouTube tech
YouTube tech form https://github.com/eXon/videojs-youtube
```html
<video id='video' class="video-js vjs-default-skin"></video>
<script src="../lib/videojs-resolution-switcher.js"></script>
<script>
videojs('video', {
controls: true,
techOrder: ["youtube"],
sources: [{ "type": "video/youtube", "src": "https://www.youtube.com/watch?v=iD_MyDbP_ZE"}],
plugins: {
videoJsResolutionSwitcher: {
default: 'low',
dynamicLabel: true
}
}
}, function(){
var player = this;
player.on('resolutionchange', function(){
console.info('Source changed')
})
});
</script>
```
### Flash tech ### Flash tech
When using flash tech `preload="auto"` is required. When using flash tech `preload="auto"` is required.
@ -102,10 +136,9 @@ videojs('video', {
### Avalible options: ### Avalible options:
* default - `{Number}|'low'|'high'` - default resolution. If any `Number` is passed plugin will try to choose source based on `res` parameter. If `low` or `high` is passed, plugin will choose respectively worse or best resolution (if `res` parameter is specified). If `res` parameter is not specified plugin assumes that sources array is sorted from best to worse. * default - `{Number}|'low'|'high'` - default resolution. If any `Number` is passed plugin will try to choose source based on `res` parameter. If `low` or `high` is passed, plugin will choose respectively worse or best resolution (if `res` parameter is specified). If `res` parameter is not specified plugin assumes that sources array is sorted from best to worse.
* dynamicLabel - `{Boolean}` - if `true` current label will be displayed in control bar. By default gear icon is displayed. * dynamicLabel - `{Boolean}` - if `true` current label will be displayed in control bar. By default gear icon is displayed.
* customSourcePicker - `{Function}` - custom function for selecting source.
* ui - `{Boolean}` - If set to `false` button will not be displayed in control bar. Default is `true`.
## Example
[Working example](example.html) of the plugin you can check out if you're having trouble. Or check out this [demo](https://kmoskwiak.github.io/videojs-resolution-switcher/).
## Methods ## Methods
@ -127,7 +160,7 @@ player.updateSrc([
|:----:|:----:|:--------:|:-----------:| |:----:|:----:|:--------:|:-----------:|
| source| array| no | array of sources | | source| array| no | array of sources |
### currentResolution([label]) ### currentResolution([label], [customSourcePicker])
If used as getter returns current resolution object: If used as getter returns current resolution object:
```javascript ```javascript
{ {
@ -154,6 +187,51 @@ player.currentResolution('SD'); // returns videojs player object
| name | type | required | description | | name | type | required | description |
|:----:|:----:|:--------:|:-----------:| |:----:|:----:|:--------:|:-----------:|
| label| string| no | label name | | label| string| no | label name |
| customSourcePicker | function | no | cutom function to choose source |
#### customSourcePicker
If there is more than one source with the same label, player will choose source automatically. This behavior can be changed if `customSourcePicker` is passed.
`customSourcePicker` must return `player` object.
```javascript
player.currentResolution('SD', function(_player, _sources, _label){
return _player.src(_sources[0]); \\ Always select first source in array
});
```
`customSourcePicker` accepst 3 arguments.
| name | type | required | description |
|:----:|:----:|:--------:|:-----------:|
| player| Object | yes | videojs player object |
| sources | Array | no | array of sources |
| label | String | no | name of label |
`customSourcePicker` may be passed in options when player is initialized:
```javascript
var myCustomSrcPicker = function(_p, _s, _l){
// select any source you want
return _p.src(selectedSource);
}
videojs('video', {
controls: true,
muted: true,
width: 1000,
plugins: {
videoJsResolutionSwitcher: {
default: 'low',
customSourcePicker: myCustomSrcPicker
}
}
}, function(){
// this is player
})
```
### getGroupedSrc()
Returns sources grouped by label, resolution and type.
## Events ## Events
@ -161,5 +239,3 @@ player.currentResolution('SD'); // returns videojs player object
### resolutionchange `EVENT` ### resolutionchange `EVENT`
> Fired when resolution is changed > Fired when resolution is changed

View file

@ -1,6 +1,6 @@
{ {
"name": "vjs-resolution-switcher", "name": "vjs-resolution-switcher",
"version": "0.2.3", "version": "0.4.2",
"authors": [ "authors": [
"Kasper Moskwiak <kasper.moskwiak@gmail.com>" "Kasper Moskwiak <kasper.moskwiak@gmail.com>"
], ],

81
examples/flash.html Normal file
View file

@ -0,0 +1,81 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Video.js Resolution Switcher</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="../node_modules/video.js/dist/video-js.css" rel="stylesheet">
<link href="../lib/videojs-resolution-switcher.css" rel="stylesheet">
<style>
body {
font-family: Arial, sans-serif;
background: #777;
}
.info {
background-color: #eee;
border: thin solid #333;
border-radius: 3px;
padding: 0 5px;
text-align: center;
}
.video-js {
margin: 40px auto;
}
</style>
</head>
<body>
<div class="info">
<p>
Use flash
</p>
</div>
<video id='video_flash' class="video-js vjs-default-skin"></video>
<script src="../node_modules/video.js/dist/video.js"></script>
<script>
videojs.options.flash.swf = "../node_modules/video.js/dist/video-js.swf"
</script>
<script src="../lib/videojs-resolution-switcher.js"></script>
<script>
// Use flash
videojs('video_flash', {
controls: true,
techOrder: ['flash'],
preload: 'auto',
width: 1000,
plugins: {
videoJsResolutionSwitcher: {
default: 'low', // Default resolution [{Number}, 'low', 'high'],
dynamicLabel: true // Display dynamic labels or gear symbol
}
}
}, function(){
var player = this;
window.player = player
player.updateSrc([
{
src: 'https://vjs.zencdn.net/v/oceans.mp4?sd',
type: 'video/mp4',
label: 'SD',
res: 360
},
{
src: 'https://vjs.zencdn.net/v/oceans.mp4?hd',
type: 'video/mp4',
label: 'HD',
res: 720
}
])
player.on('resolutionchange', function(){
console.info('Source changed to %s', player.src())
})
})
</script>
</body>
</html>

65
examples/hls.html Normal file
View file

@ -0,0 +1,65 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Video.js Resolution Switcher</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="../node_modules/video.js/dist/video-js.css" rel="stylesheet">
<link href="../lib/videojs-resolution-switcher.css" rel="stylesheet">
<style>
body {
font-family: Arial, sans-serif;
background: #777;
}
.info {
background-color: #eee;
border: thin solid #333;
border-radius: 3px;
padding: 0 5px;
text-align: center;
}
.video-js {
margin: 40px auto;
}
</style>
</head>
<body>
<div class="info">
<p>
HLS tech
</p>
</div>
<video id="video" class="video-js"></video>
<script src="../node_modules/video.js/dist/video.js"></script>
<script src="../node_modules/videojs-contrib-hls/dist/videojs-contrib-hls.min.js"></script>
<script src="../lib/videojs-resolution-switcher.js"></script>
<script>
// fire up the plugin
videojs('video', {
controls: true,
muted: true,
width: 1000,
plugins: {
videoJsResolutionSwitcher: {
dynamicLabel: true // Display dynamic labels or gear symbol
}
}
}, function(){
var player = this;
window.player = player;
player.updateSrc({
src: '//labs.tvpw.pl/video/hls/tears.m3u8',
type: 'application/x-mpegURL'
}, {hls: true});
player.on('resolutionchange', function(){
console.info('Source changed to %s', player.src());
});
})
</script>
</body>
</html>

View file

@ -4,8 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>Video.js Resolution Switcher</title> <title>Video.js Resolution Switcher</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link href="node_modules/video.js/dist/video-js.css" rel="stylesheet"> <link href="../node_modules/video.js/dist/video-js.css" rel="stylesheet">
<link href="lib/videojs-resolution-switcher.css" rel="stylesheet"> <link href="../lib/videojs-resolution-switcher.css" rel="stylesheet">
<style> <style>
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
@ -45,19 +45,12 @@
<source src="https://vjs.zencdn.net/v/oceans.mp4?2160" type='video/mp4' label='4k' res='2160'/> <source src="https://vjs.zencdn.net/v/oceans.mp4?2160" type='video/mp4' label='4k' res='2160'/>
</video> </video>
<div class="info">
<p>
Use flash
</p>
</div>
<video id='video_flash' class="video-js vjs-default-skin"></video> <script src="../node_modules/video.js/dist/video.js"></script>
<script src="node_modules/video.js/dist/video.js"></script>
<script> <script>
videojs.options.flash.swf = "node_modules/video.js/dist/video-js.swf" videojs.options.flash.swf = "../node_modules/video.js/dist/video-js.swf"
</script> </script>
<script src="lib/videojs-resolution-switcher.js"></script> <script src="../lib/videojs-resolution-switcher.js"></script>
<script> <script>
// fire up the plugin // fire up the plugin
videojs('video', { videojs('video', {
@ -66,6 +59,7 @@
width: 1000, width: 1000,
plugins: { plugins: {
videoJsResolutionSwitcher: { videoJsResolutionSwitcher: {
ui: true,
default: 'low', // Default resolution [{Number}, 'low', 'high'], default: 'low', // Default resolution [{Number}, 'low', 'high'],
dynamicLabel: true // Display dynamic labels or gear symbol dynamicLabel: true // Display dynamic labels or gear symbol
} }
@ -94,45 +88,6 @@
}) })
}) })
// Use flash
videojs('video_flash', {
controls: true,
techOrder: ['flash'],
preload: 'auto',
width: 1000,
plugins: {
videoJsResolutionSwitcher: {
default: 'low', // Default resolution [{Number}, 'low', 'high'],
dynamicLabel: true // Display dynamic labels or gear symbol
}
}
}, function(){
var player = this;
window.player = player
player.updateSrc([
{
src: 'https://vjs.zencdn.net/v/oceans.mp4?sd',
type: 'video/mp4',
label: 'SD',
res: 360
},
{
src: 'https://vjs.zencdn.net/v/oceans.mp4?hd',
type: 'video/mp4',
label: 'HD',
res: 720
}
])
player.on('resolutionchange', function(){
console.info('Source changed to %s', player.src())
})
})
</script> </script>

64
examples/youtube.html Normal file
View file

@ -0,0 +1,64 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Video.js Resolution Switcher</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="../node_modules/video.js/dist/video-js.css" rel="stylesheet">
<link href="../lib/videojs-resolution-switcher.css" rel="stylesheet">
<style>
body {
font-family: Arial, sans-serif;
background: #777;
}
.info {
background-color: #eee;
border: thin solid #333;
border-radius: 3px;
padding: 0 5px;
text-align: center;
}
.video-js {
margin: 40px auto;
}
</style>
</head>
<body>
<div class="info">
<p>
Youtube tech
</p>
</div>
<video id='video' class="video-js vjs-default-skin"></video>
<script src="../node_modules/video.js/dist/video.js"></script>
<script src="../node_modules/videojs-youtube/dist/Youtube.js"></script>
<script src="../lib/videojs-resolution-switcher.js"></script>
<script>
// fire up the plugin
videojs('video', {
controls: true,
muted: true,
techOrder: ["youtube"],
width: 500,
sources: [{ "type": "video/youtube", "src": "https://www.youtube.com/watch?v=iD_MyDbP_ZE"}],
plugins: {
videoJsResolutionSwitcher: {
default: 'low',
dynamicLabel: true
}
}
}, function(){
var player = this;
window.player = player;
player.on('resolutionchange', function(){
console.info('Source changed')
})
});
</script>
</body>
</html>

View file

@ -1,30 +1,31 @@
.vjs-resolution-button { .vjs-resolution-button .vjs-menu-icon:before {
color: #ccc;
font-family: VideoJS;
}
.vjs-resolution-button .vjs-resolution-button-staticlabel:before {
content: '\f110'; content: '\f110';
font-family: VideoJS;
font-weight: normal;
font-style: normal;
font-size: 1.8em; font-size: 1.8em;
line-height: 1.67; line-height: 1.67em;
} }
.vjs-resolution-button .vjs-resolution-button-label { .vjs-resolution-button .vjs-resolution-button-label {
font-size: 1.2em; font-size: 1em;
line-height: 2.50em; line-height: 3em;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: center;
box-sizing: inherit;
}
.vjs-resolution-button ul.vjs-menu-content {
width: 4em !important;
}
.vjs-resolution-button .vjs-menu {
left: 0; left: 0;
width: 100%;
height: 100%;
text-align: center;
box-sizing: inherit;
}
.vjs-resolution-button .vjs-menu .vjs-menu-content {
width: 4em;
left: 50%; /* Center the menu, in it's parent */
margin-left: -2em; /* half of width, to center */
}
.vjs-resolution-button .vjs-menu li {
text-transform: none;
font-size: 1em;
} }

View file

@ -1,6 +1,6 @@
/*! videojs-resolution-switcher - v0.0.0 - 2015-7-26 /*! videojs-resolution-switcher - 2015-7-26
* Copyright (c) 2015 Kasper Moskwiak * Copyright (c) 2016 Kasper Moskwiak
* Modified by Pierre Kraft * Modified by Pierre Kraft and Derk-Jan Hartman
* Licensed under the Apache-2.0 license. */ * Licensed under the Apache-2.0 license. */
(function() { (function() {
@ -15,136 +15,89 @@
} }
(function(window, videojs) { (function(window, videojs) {
var videoJsResolutionSwitcher,
defaults = {
var defaults = {}, ui: true
videoJsResolutionSwitcher,
currentResolution = {}, // stores current resolution
menuItemsHolder = {}; // stores menuItems
function setSourcesSanitized(player, sources, label, customSourcePicker) {
currentResolution = {
label: label,
sources: sources
}; };
if(typeof customSourcePicker === 'function'){
return customSourcePicker(player, sources, label);
}
return player.src(sources.map(function(src) {
return {src: src.src, type: src.type, res: src.res};
}));
}
/* /*
* Resolution menu item * Resolution menu item
*/ */
var MenuItem = videojs.getComponent('MenuItem'); var MenuItem = videojs.getComponent('MenuItem');
var ResolutionMenuItem = videojs.extend(MenuItem, { var ResolutionMenuItem = videojs.extend(MenuItem, {
constructor: function(player, options, onClickListener, label){ constructor: function(player, options){
this.onClickListener = onClickListener; options.selectable = true;
this.label = label; // Sets this.player_, this.options_ and initializes the component
// Sets this.player_, this.options_ and initializes the component MenuItem.call(this, player, options);
MenuItem.call(this, player, options); this.src = options.src;
this.src = options.src;
this.on('click', this.onClick); player.on('resolutionchange', videojs.bind(this, this.update));
this.on('touchstart', this.onClick);
if (options.initialySelected) {
this.showAsLabel();
this.selected(true);
} }
}, } );
showAsLabel: function() { ResolutionMenuItem.prototype.handleClick = function(event){
// Change menu button label to the label of this item if the menu button label is provided MenuItem.prototype.handleClick.call(this,event);
if(this.label) { this.player_.currentResolution(this.options_.label);
this.label.innerHTML = this.options_.label; };
} ResolutionMenuItem.prototype.update = function(){
}, var selection = this.player_.currentResolution();
onClick: function(customSourcePicker){ this.selected(this.options_.label === selection.label);
this.onClickListener(this); };
// Remember player state MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem);
var currentTime = this.player_.currentTime();
var isPaused = this.player_.paused();
this.showAsLabel();
// Hide bigPlayButton
if(!isPaused){
this.player_.bigPlayButton.hide();
}
if(typeof customSourcePicker !== 'function' &&
typeof this.options_.customSourcePicker === 'function'){
customSourcePicker = this.options_.customSourcePicker;
}
// Change player source and wait for loadeddata event, then play video
// loadedmetadata doesn't work right now for flash.
// Probably because of https://github.com/videojs/video-js-swf/issues/124
// If player preload is 'none' and then loadeddata not fired. So, we need timeupdate event for seek handle (timeupdate doesn't work properly with flash)
var handleSeekEvent = 'loadeddata';
if(this.player_.preload() == 'none' && this.player_.techName_ != 'Flash') {
handleSeekEvent = 'timeupdate';
}
setSourcesSanitized(this.player_, this.src, this.options_.label, customSourcePicker).one(handleSeekEvent, function() {
this.player_.currentTime(currentTime);
this.player_.handleTechSeeked_();
if(!isPaused){
// Start playing and hide loadingSpinner (flash issue ?)
this.player_.play().handleTechSeeked_();
}
this.player_.trigger('resolutionchange');
});
}
});
/* /*
* Resolution menu button * Resolution menu button
*/ */
var MenuButton = videojs.getComponent('MenuButton'); var MenuButton = videojs.getComponent('MenuButton');
var ResolutionMenuButton = videojs.extend(MenuButton, { var ResolutionMenuButton = videojs.extend(MenuButton, {
constructor: function(player, options, settings, label){ constructor: function(player, options){
this.sources = options.sources; this.label = document.createElement('span');
this.label = label; options.label = 'Quality';
this.label.innerHTML = options.initialySelectedLabel;
// Sets this.player_, this.options_ and initializes the component // Sets this.player_, this.options_ and initializes the component
MenuButton.call(this, player, options, settings); MenuButton.call(this, player, options);
this.el().setAttribute('aria-label','Quality');
this.controlText('Quality'); this.controlText('Quality');
if(settings.dynamicLabel){ if(options.dynamicLabel){
this.el().appendChild(label); videojs.addClass(this.label, 'vjs-resolution-button-label');
this.el().appendChild(this.label);
}else{ }else{
var staticLabel = document.createElement('span'); var staticLabel = document.createElement('span');
staticLabel.classList.add('vjs-resolution-button-staticlabel'); videojs.addClass(staticLabel, 'vjs-menu-icon');
this.el().appendChild(staticLabel); this.el().appendChild(staticLabel);
} }
}, player.on('updateSources', videojs.bind( this, this.update ) );
createItems: function(){ }
var menuItems = []; } );
var labels = (this.sources && this.sources.label) || {}; ResolutionMenuButton.prototype.createItems = function(){
var onClickUnselectOthers = function(clickedItem) { var menuItems = [];
menuItems.map(function(item) { var labels = (this.sources && this.sources.label) || {};
item.selected(item === clickedItem);
});
};
for (var key in labels) { // FIXME order is not guaranteed here.
if (labels.hasOwnProperty(key)) { for (var key in labels) {
menuItems.push(new ResolutionMenuItem( if (labels.hasOwnProperty(key)) {
this.player_, menuItems.push(new ResolutionMenuItem(
{ this.player_,
label: key, {
src: labels[key], label: key,
initialySelected: key === this.options_.initialySelectedLabel, src: labels[key],
customSourcePicker: this.options_.customSourcePicker selected: key === (this.currentSelection ? this.currentSelection.label : false)
}, })
onClickUnselectOthers, );
this.label)); }
// Store menu item for API calls }
menuItemsHolder[key] = menuItems[menuItems.length - 1];
} return menuItems;
} };
return menuItems; ResolutionMenuButton.prototype.update = function(){
} this.sources = this.player_.getGroupedSrc();
}); this.currentSelection = this.player_.currentResolution();
this.label.innerHTML = this.currentSelection ? this.currentSelection.label : '';
return MenuButton.prototype.update.call(this);
};
ResolutionMenuButton.prototype.buildCSSClass = function(){
return MenuButton.prototype.buildCSSClass.call( this ) + ' vjs-resolution-button';
};
MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton);
/** /**
* Initialize the plugin. * Initialize the plugin.
@ -153,51 +106,116 @@
videoJsResolutionSwitcher = function(options) { videoJsResolutionSwitcher = function(options) {
var settings = videojs.mergeOptions(defaults, options), var settings = videojs.mergeOptions(defaults, options),
player = this, player = this,
label = document.createElement('span'), groupedSrc = {},
groupedSrc = {}; currentSources = {},
currentResolutionState = {};
label.classList.add('vjs-resolution-button-label');
/** /**
* Updates player sources or returns current source URL * Updates player sources or returns current source URL
* @param {Array} [src] array of sources [{src: '', type: '', label: '', res: ''}] * @param {Array} [src] array of sources [{src: '', type: '', label: '', res: ''}]
* @returns {Object|String|Array} videojs player object if used as setter or current source URL, object, or array of sources * @returns {Object|String|Array} videojs player object if used as setter or current source URL, object, or array of sources
*/ */
player.updateSrc = function(src){ player.updateSrc = function(src, _options){
//Return current src if src is not given //Return current src if src is not given
if(!src){ return player.src(); } if(!src){ return player.src(); }
// Dispose old resolution menu button before adding new sources
if(player.controlBar.resolutionSwitcher){ if(_options && _options.hls){
player.controlBar.resolutionSwitcher.dispose(); player.src(src);
delete player.controlBar.resolutionSwitcher; return initResolutionForHLS(player);
} }
// Only add those sources which we can (maybe) play
src = src.filter( function(source) {
try {
return ( player.canPlayType( source.type ) !== '' );
} catch (e) {
// If a Tech doesn't yet have canPlayType just add it
return true;
}
});
//Sort sources //Sort sources
src = src.sort(compareResolutions); this.currentSources = src.sort(compareResolutions);
groupedSrc = bucketSources(src); this.groupedSrc = bucketSources(this.currentSources);
var choosen = chooseSrc(groupedSrc, src); // Pick one by default
var menuButton = new ResolutionMenuButton(player, { sources: groupedSrc, initialySelectedLabel: choosen.label , initialySelectedRes: choosen.res , customSourcePicker: settings.customSourcePicker}, settings, label); var chosen = chooseSrc(this.groupedSrc, this.currentSources);
menuButton.el().classList.add('vjs-resolution-button'); this.currentResolutionState = {
player.controlBar.resolutionSwitcher = player.controlBar.addChild(menuButton); label: chosen.label,
return setSourcesSanitized(player, choosen.sources, choosen.label); sources: chosen.sources
};
player.trigger('updateSources');
player.setSourcesSanitized(chosen.sources, chosen.label);
player.trigger('resolutionchange');
return player;
}; };
/** /**
* Returns current resolution or sets one when label is specified * Returns current resolution or sets one when label is specified
* @param {String} [label] label name * @param {String} [label] label name
* @param {Function} [customSourcePicker] custom function to choose source. Takes 3 arguments: player, sources, label. Must return player object. * @param {Function} [customSourcePicker] custom function to choose source. Takes 2 arguments: sources, label. Must return player object.
* @returns {Object} current resolution object {label: '', sources: []} if used as getter or player object if used as setter * @returns {Object} current resolution object {label: '', sources: []} if used as getter or player object if used as setter
*/ */
player.currentResolution = function(label, customSourcePicker){ player.currentResolution = function(label, customSourcePicker){
if(label == null) { return currentResolution; } if(label == null) { return this.currentResolutionState; }
if(menuItemsHolder[label] != null){
menuItemsHolder[label].onClick(customSourcePicker); // Lookup sources for label
if(!this.groupedSrc || !this.groupedSrc.label || !this.groupedSrc.label[label]){
return;
} }
var sources = this.groupedSrc.label[label];
// Remember player state
var currentTime = player.currentTime();
var isPaused = player.paused();
// Hide bigPlayButton
if(!isPaused && this.player_.options_.bigPlayButton){
this.player_.bigPlayButton.hide();
}
// Change player source and wait for loadeddata event, then play video
// loadedmetadata doesn't work right now for flash.
// Probably because of https://github.com/videojs/video-js-swf/issues/124
// If player preload is 'none' and then loadeddata not fired. So, we need timeupdate event for seek handle (timeupdate doesn't work properly with flash)
var handleSeekEvent = 'loadeddata';
if(this.player_.techName_ !== 'Youtube' && this.player_.preload() === 'none' && this.player_.techName_ !== 'Flash') {
handleSeekEvent = 'timeupdate';
}
player
.setSourcesSanitized(sources, label, customSourcePicker || settings.customSourcePicker)
.one(handleSeekEvent, function() {
player.currentTime(currentTime);
player.handleTechSeeked_();
if(!isPaused){
// Start playing and hide loadingSpinner (flash issue ?)
player.play().handleTechSeeked_();
}
player.trigger('resolutionchange');
});
return player; return player;
}; };
/**
* Returns grouped sources by label, resolution and type
* @returns {Object} grouped sources: { label: { key: [] }, res: { key: [] }, type: { key: [] } }
*/
player.getGroupedSrc = function(){ player.getGroupedSrc = function(){
return groupedSrc; return this.groupedSrc;
} };
player.setSourcesSanitized = function(sources, label, customSourcePicker) {
this.currentResolutionState = {
label: label,
sources: sources
};
if(typeof customSourcePicker === 'function'){
return customSourcePicker(player, sources, label);
}
player.src(sources.map(function(src) {
return {src: src.src, type: src.type, res: src.res};
}));
return player;
};
/** /**
* Method used for sorting list of sources * Method used for sorting list of sources
@ -255,7 +273,7 @@
if (selectedRes === 'high') { if (selectedRes === 'high') {
selectedRes = src[0].res; selectedRes = src[0].res;
selectedLabel = src[0].label; selectedLabel = src[0].label;
} else if (selectedRes === 'low' || selectedRes == null) { } else if (selectedRes === 'low' || selectedRes == null || !groupedSrc.res[selectedRes]) {
// Select low-res if default is low or not set // Select low-res if default is low or not set
selectedRes = src[src.length - 1].res; selectedRes = src[src.length - 1].res;
selectedLabel = src[src.length -1].label; selectedLabel = src[src.length -1].label;
@ -263,20 +281,165 @@
selectedLabel = groupedSrc.res[selectedRes][0].label; selectedLabel = groupedSrc.res[selectedRes][0].label;
} }
if(selectedRes === undefined){
return {res: selectedRes, label: selectedLabel, sources: groupedSrc.label[selectedLabel]};
}
return {res: selectedRes, label: selectedLabel, sources: groupedSrc.res[selectedRes]}; return {res: selectedRes, label: selectedLabel, sources: groupedSrc.res[selectedRes]};
} }
// Create resolution switcher for videos form <source> tag inside <video> function initResolutionForYt(player){
if(player.options_.sources.length > 1){ // Map youtube qualities names
// Wait for player ready event var _yts = {
player.ready(function(){ highres: {res: 1080, label: '1080', yt: 'highres'},
player.updateSrc(player.options_.sources); hd1080: {res: 1080, label: '1080', yt: 'hd1080'},
hd720: {res: 720, label: '720', yt: 'hd720'},
large: {res: 480, label: '480', yt: 'large'},
medium: {res: 360, label: '360', yt: 'medium'},
small: {res: 240, label: '240', yt: 'small'},
tiny: {res: 144, label: '144', yt: 'tiny'},
auto: {res: 0, label: 'auto', yt: 'auto'}
};
// Overwrite default sourcePicker function
var _customSourcePicker = function(_player, _sources, _label){
// Note that setPlayebackQuality is a suggestion. YT does not always obey it.
player.tech_.ytPlayer.setPlaybackQuality(_sources[0]._yt);
player.trigger('updateSources');
return player;
};
settings.customSourcePicker = _customSourcePicker;
// Init resolution
player.tech_.ytPlayer.setPlaybackQuality('auto');
// This is triggered when the resolution actually changes
player.tech_.ytPlayer.addEventListener('onPlaybackQualityChange', function(event){
for(var res in _yts) {
if(res.yt === event.data) {
player.currentResolution(res.label, _customSourcePicker);
return;
}
}
});
// We must wait for play event
player.one('play', function(){
var qualities = player.tech_.ytPlayer.getAvailableQualityLevels();
var _sources = [];
qualities.map(function(q){
_sources.push({
src: player.src().src,
type: player.src().type,
label: _yts[q].label,
res: _yts[q].res,
_yt: _yts[q].yt
});
});
player.groupedSrc = bucketSources(_sources);
var chosen = {label: 'auto', res: 0, sources: player.groupedSrc.label.auto};
this.currentResolutionState = {
label: chosen.label,
sources: chosen.sources
};
player.trigger('updateSources');
player.setSourcesSanitized(chosen.sources, chosen.label, _customSourcePicker);
}); });
} }
function initResolutionForHLS(player){
var hls = player.tech_.hls;
if(!hls) { return; }
// Capture events
player.on('mediachange', function(){
player.trigger('resolutionchange');
});
player.one('loadedmetadata', function(){
var representations = hls.representations();
var _sources = [{ src: 'auto', type: 'application/x-mpegURL', label: 'auto', res: 0}];
representations.map(function(representation){
_sources.push({
src: representation,
type: 'application/x-mpegURL',
label: representation.height,
res: representation.height
});
});
player.groupedSrc = bucketSources(_sources);
// Overwrite defualt sourcePicer function
var _customSourcePicker = function(_player, _sources, _label){
var representations = _player.tech_.hls.representations();
if(_label === 'auto'){
// If user selected auto enable all representations
representations.map(function(represenation){
represenation.enabled(true);
});
player.trigger('updateSources');
return _player;
}
var source = _sources[0];
var id = source.src.id;
representations.map(function(represenation){
if(represenation.id !== id){
represenation.enabled(false);
}else{
represenation.enabled(true);
}
});
player.trigger('updateSources');
return _player;
};
settings.customSourcePicker = _customSourcePicker;
var currentPlaylist = hls.playlists.media();
var chosen = {
label: 'auto',
res: currentPlaylist.attributes.RESOLUTION.height,
sources: currentPlaylist
};
this.currentResolutionState = {
label: chosen.label,
sources: chosen.sources
};
player.trigger('updateSources');
player.setSourcesSanitized(chosen.sources, chosen.label, _customSourcePicker);
});
}
player.ready(function(){
if( settings.ui ) {
var menuButton = new ResolutionMenuButton(player, settings);
player.controlBar.resolutionSwitcher = player.controlBar.el_.insertBefore(menuButton.el_, player.controlBar.getChild('fullscreenToggle').el_);
player.controlBar.resolutionSwitcher.dispose = function(){
this.parentNode.removeChild(this);
};
}
if(player.options_.sources.length > 1){
// tech: Html5 and Flash
// Create resolution switcher for videos form <source> tag inside <video>
player.updateSrc(player.options_.sources);
}
if(player.tech_.hls && player.options_.sources.length){
// HLS support
initResolutionForHLS(player);
}
if(player.techName_ === 'Youtube'){
// tech: YouTube
initResolutionForYt(player);
}
});
}; };
// register the plugin // register the plugin

View file

@ -1,6 +1,6 @@
{ {
"name": "videojs-resolution-switcher", "name": "videojs-resolution-switcher",
"version": "0.2.3", "version": "0.4.2",
"main": "./lib/videojs-resolution-switcher.js", "main": "./lib/videojs-resolution-switcher.js",
"author": { "author": {
"name": "Kasper Moskwiak", "name": "Kasper Moskwiak",
@ -18,7 +18,7 @@
"url": "git@github.com:kmoskwiak/videojs-resolution-switcher.git" "url": "git@github.com:kmoskwiak/videojs-resolution-switcher.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/kmoskwiak/videojs-resolution-switcher/issues" "url": "https://github.com/khmoskwiak/videojs-resolution-switcher/issues"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"keywords": [ "keywords": [
@ -31,18 +31,23 @@
"source", "source",
"videojs-plugin" "videojs-plugin"
], ],
"dependencies": {}, "scripts": {
"test": "grunt test"
},
"devDependencies": { "devDependencies": {
"grunt-contrib-clean": "^0.7", "grunt": "^1.0.1",
"grunt-contrib-concat": "^0.5", "grunt-contrib-clean": "^1.0",
"grunt-contrib-jshint": "^0.11", "grunt-contrib-concat": "^1.0",
"grunt-contrib-qunit": "^0.7", "grunt-contrib-jshint": "^1.0",
"grunt-contrib-uglify": "^0.11", "grunt-contrib-qunit": "^1.1",
"grunt-contrib-watch": "^0.6", "grunt-contrib-uglify": "^1.0",
"video.js": "^5.4", "grunt-contrib-watch": "^1.0",
"qunitjs": "^1.20" "video.js": "^5.13",
"qunitjs": "^2.0.0-rc1",
"videojs-youtube": "^2.0.8",
"videojs-contrib-hls": "^3.5.3"
}, },
"peerDependencies": { "peerDependencies": {
"video.js": "^5.4" "video.js": "^5.13"
} }
} }