diff --git a/.gitignore b/.gitignore
index 032441d..f436212 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.DS_Store
node_modules
coverage
+dist
diff --git a/.npmignore b/.npmignore
index 55890a2..b6478af 100644
--- a/.npmignore
+++ b/.npmignore
@@ -2,3 +2,4 @@
.travis.yml
Gruntfile.js
tests/**
+docs
diff --git a/Gruntfile.js b/Gruntfile.js
index 3f766dc..ac3fd8b 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -5,29 +5,145 @@
'use strict';
+var path = require('path'),
+ getCodeVersion = require('silvermine-serverless-utils/src/get-code-version');
+
module.exports = function(grunt) {
- var config;
+ var DEBUG = !!grunt.option('debug'),
+ config;
config = {
js: {
all: [ 'Gruntfile.js', 'src/**/*.js', 'tests/**/*.js' ],
+ standalone: path.join(__dirname, 'src', 'js', 'standalone.js'),
},
+
+ sass: {
+ base: path.join(__dirname, 'src', 'sass'),
+ all: [ 'src/**/*.scss' ],
+ },
+
+ dist: {
+ base: path.join(__dirname, 'dist'),
+ },
+ };
+
+ config.dist.js = {
+ bundle: path.join(config.dist.base, 'js', '<%= pkg.name %>.js'),
+ minified: path.join(config.dist.base, 'js', '<%= pkg.name %>.min.js'),
+ };
+
+ config.dist.css = {
+ base: path.join(config.dist.base, 'css'),
+ all: path.join(config.dist.base, '**', '*.css'),
};
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
+ versionInfo: getCodeVersion.both(),
+ config: config,
+
+ browserify: {
+ main: {
+ src: config.js.standalone,
+ dest: config.dist.js.bundle,
+ },
+ },
+
+ uglify: {
+ main: {
+ files: {
+ '<%= config.dist.js.minified %>': config.dist.js.bundle,
+ },
+ options: {
+ banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> <%= versionInfo %> */\n',
+ sourceMap: true,
+ sourceMapIncludeSources: true,
+ mangle: true,
+ compress: true,
+ beautify: false,
+ },
+ },
+ },
+
+ sass: {
+ options: {
+ sourceMap: DEBUG,
+ indentWidth: 3,
+ outputStyle: DEBUG ? 'expanded' : 'compressed',
+ sourceComments: DEBUG,
+ },
+ main: {
+ files: [
+ {
+ expand: true,
+ cwd: config.sass.base,
+ src: [ '**/*.scss' ],
+ dest: config.dist.css.base,
+ ext: '.css',
+ extDot: 'first',
+ },
+ ],
+ },
+ },
+
+ postcss: {
+ options: {
+ map: DEBUG,
+ processors: [
+ require('autoprefixer')({ browsers: '> .05%' }), // eslint-disable-line global-require
+ ],
+ },
+ main: {
+ src: config.dist.css.all,
+ },
+ },
eslint: {
target: config.js.all,
},
+ sasslint: {
+ options: {
+ configFile: path.join(__dirname, 'node_modules', 'sass-lint-config-silvermine', 'sass-lint.yml'),
+ },
+ target: config.sass.all,
+ },
+
+ watch: {
+ grunt: {
+ files: [ 'Gruntfile.js' ],
+ tasks: [ 'build' ],
+ },
+
+ js: {
+ files: [ 'src/**/*.js' ],
+ tasks: [ 'build-js' ],
+ },
+
+ css: {
+ files: [ 'src/**/*.scss' ],
+ tasks: [ 'build-css' ],
+ },
+ },
+
});
+ grunt.loadNpmTasks('grunt-contrib-uglify');
+ grunt.loadNpmTasks('grunt-contrib-watch');
+ grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-eslint');
+ grunt.loadNpmTasks('grunt-postcss');
+ grunt.loadNpmTasks('grunt-sass');
+ grunt.loadNpmTasks('grunt-sass-lint');
- grunt.registerTask('standards', [ 'eslint' ]);
+ grunt.registerTask('standards', [ 'eslint', 'sasslint' ]);
+ grunt.registerTask('build-js', [ 'browserify', 'uglify' ]);
+ grunt.registerTask('build-css', [ 'sass', 'postcss' ]);
+ grunt.registerTask('build', [ 'build-js', 'build-css' ]);
+ grunt.registerTask('develop', [ 'build', 'watch' ]);
grunt.registerTask('default', [ 'standards' ]);
};
diff --git a/README.md b/README.md
index 5d3e7b0..319a57d 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
# Silvermine VideoJS Quality/Resolution Selector
-[![Build Status](https://travis-ci.org/silvermine/videojs-quality-selector.png?branch=master)](https://travis-ci.org/silvermine/videojs-quality-selector)
+[![Build Status](https://travis-ci.org/silvermine/videojs-quality-selector.svg?branch=master)](https://travis-ci.org/silvermine/videojs-quality-selector)
[![Coverage Status](https://coveralls.io/repos/github/silvermine/videojs-quality-selector/badge.svg?branch=master)](https://coveralls.io/github/silvermine/videojs-quality-selector?branch=master)
-[![Dependency Status](https://david-dm.org/silvermine/videojs-quality-selector.png)](https://david-dm.org/silvermine/videojs-quality-selector)
-[![Dev Dependency Status](https://david-dm.org/silvermine/videojs-quality-selector/dev-status.png)](https://david-dm.org/silvermine/videojs-quality-selector#info=devDependencies&view=table)
+[![Dependency Status](https://david-dm.org/silvermine/videojs-quality-selector.svg)](https://david-dm.org/silvermine/videojs-quality-selector)
+[![Dev Dependency Status](https://david-dm.org/silvermine/videojs-quality-selector/dev-status.svg)](https://david-dm.org/silvermine/videojs-quality-selector?type=dev)
## What is it?
diff --git a/docs/demo/index.html b/docs/demo/index.html
new file mode 100644
index 0000000..7011c5c
--- /dev/null
+++ b/docs/demo/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+ videojs-quality-selector Demo
+
+
+
+
+
+
+ Demo of videojs-quality-selector
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index e3285e3..85105f6 100644
--- a/package.json
+++ b/package.json
@@ -2,8 +2,9 @@
"name": "silvermine-videojs-quality-selector",
"version": "0.9.0",
"description": "video.js plugin for selecting a video quality or resolution",
- "main": "src/index.js",
+ "main": "src/js/index.js",
"scripts": {
+ "prepublish": "grunt build",
"test": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- -R spec 'tests/**/*.test.js'"
},
"author": "Jeremy Thomerson",
@@ -24,17 +25,26 @@
},
"homepage": "https://github.com/silvermine/videojs-quality-selector#readme",
"devDependencies": {
+ "autoprefixer": "7.1.1",
"class.extend": "0.9.2",
"coveralls": "2.13.1",
"eslint": "4.0.0",
"eslint-config-silvermine": "1.3.0",
"expect.js": "0.3.1",
"grunt": "1.0.1",
+ "grunt-browserify": "5.0.0",
+ "grunt-contrib-uglify": "3.0.1",
+ "grunt-contrib-watch": "1.0.0",
"grunt-eslint": "20.0.0",
+ "grunt-postcss": "0.8.0",
+ "grunt-sass": "2.0.0",
+ "grunt-sass-lint": "0.2.2",
"istanbul": "0.4.5",
"mocha": "3.4.2",
"mocha-lcov-reporter": "1.3.0",
"rewire": "2.5.2",
+ "sass-lint-config-silvermine": "1.0.1",
+ "silvermine-serverless-utils": "git+https://github.com/silvermine/serverless-utils.git#910f1149af824fc8d0fa840878079c7d3df0f414",
"sinon": "2.3.5",
"underscore": "1.8.3"
}
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index 8b46fbb..0000000
--- a/src/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-'use strict';
-
-module.exports = {};
diff --git a/src/js/components/QualityOption.js b/src/js/components/QualityOption.js
new file mode 100644
index 0000000..92bd497
--- /dev/null
+++ b/src/js/components/QualityOption.js
@@ -0,0 +1,46 @@
+'use strict';
+
+var _ = require('underscore'),
+ events = require('../events');
+
+module.exports = function(videojs) {
+ var MenuItem = videojs.getComponent('MenuItem');
+
+ /**
+ * A MenuItem to represent a video resolution
+ *
+ * @class QualityOption
+ * @extends videojs.MenuItem
+ */
+ return videojs.extend(MenuItem, {
+
+ /**
+ * @inheritdoc
+ */
+ constructor: function(player, options) {
+ var source = options.source;
+
+ if (!_.isObject(source)) {
+ throw new Error('was not provided a "source" object, but rather: ' + (typeof source));
+ }
+
+ options = _.extend({
+ selectable: true,
+ label: source.label,
+ }, options);
+
+ MenuItem.call(this, player, options);
+
+ this.source = source;
+ },
+
+ /**
+ * @inheritdoc
+ */
+ handleClick: function(event) {
+ MenuItem.prototype.handleClick.call(this, event);
+ this.player().trigger(events.QUALITY_SELECTED, this.source);
+ },
+
+ });
+};
diff --git a/src/js/components/QualitySelector.js b/src/js/components/QualitySelector.js
new file mode 100644
index 0000000..72e6ac8
--- /dev/null
+++ b/src/js/components/QualitySelector.js
@@ -0,0 +1,79 @@
+'use strict';
+
+var _ = require('underscore'),
+ events = require('../events'),
+ qualityOptionFactory = require('./QualityOption');
+
+module.exports = function(videojs) {
+ var MenuButton = videojs.getComponent('MenuButton'),
+ QualityOption = qualityOptionFactory(videojs),
+ QualitySelector;
+
+ /**
+ * A component for changing video resolutions
+ *
+ * @class QualitySelector
+ * @extends videojs.Button
+ */
+ QualitySelector = videojs.extend(MenuButton, {
+
+ /**
+ * @inheritdoc
+ */
+ constructor: function(player, options) {
+ MenuButton.call(this, player, options);
+
+ player.on(events.QUALITY_SELECTED, function(event, source) {
+ this.setSelectedSource(source);
+ }.bind(this));
+
+ // Since it's possible for the player to get a source before the selector is
+ // created, make sure to update once we get a "ready" signal.
+ player.one('ready', function() {
+ this.selectedSrc = player.src();
+ this.update();
+ }.bind(this));
+ },
+
+ /**
+ * Updates the source that is selected in the menu
+ *
+ * @param source {object} player source to display as selected
+ */
+ setSelectedSource: function(source) {
+ this.selectedSrc = source ? source.src : undefined;
+ this.update();
+ },
+
+ /**
+ * @inheritdoc
+ */
+ createItems: function() {
+ var player = this.player(),
+ sources = player.currentSources();
+
+ if (!sources || sources.length < 2) {
+ return [];
+ }
+
+ return _.map(sources, function(source) {
+ return new QualityOption(player, {
+ source: source,
+ selected: source.src === this.selectedSrc,
+ });
+ }.bind(this));
+ },
+
+ /**
+ * @inheritdoc
+ */
+ buildWrapperCSSClass: function() {
+ return 'vjs-quality-selector ' + MenuButton.prototype.buildWrapperCSSClass.call(this);
+ },
+
+ });
+
+ videojs.registerComponent('QualitySelector', QualitySelector);
+
+ return QualitySelector;
+};
diff --git a/src/js/events.js b/src/js/events.js
new file mode 100644
index 0000000..2301e92
--- /dev/null
+++ b/src/js/events.js
@@ -0,0 +1,7 @@
+'use strict';
+
+module.exports = {
+
+ QUALITY_SELECTED: 'qualitySelected',
+
+};
diff --git a/src/js/index.js b/src/js/index.js
new file mode 100644
index 0000000..0004740
--- /dev/null
+++ b/src/js/index.js
@@ -0,0 +1,14 @@
+'use strict';
+
+var events = require('./events'),
+ qualitySelectorFactory = require('./components/QualitySelector'),
+ sourceInterceptorFactory = require('./middleware/SourceInterceptor');
+
+module.exports = function(videojs) {
+ videojs = videojs || window.videojs;
+
+ qualitySelectorFactory(videojs);
+ sourceInterceptorFactory(videojs);
+};
+
+module.exports.EVENTS = events;
diff --git a/src/js/middleware/SourceInterceptor.js b/src/js/middleware/SourceInterceptor.js
new file mode 100644
index 0000000..d2223ca
--- /dev/null
+++ b/src/js/middleware/SourceInterceptor.js
@@ -0,0 +1,73 @@
+'use strict';
+
+var _ = require('underscore'),
+ events = require('../events'),
+ QUALITY_CHANGE_CLASS = 'vjs-quality-changing';
+
+module.exports = function(videojs) {
+
+ videojs.use('*', function(player) {
+
+ player.on(events.QUALITY_SELECTED, function(event, newSource) {
+ var sources = player.currentSources(),
+ currentTime = player.currentTime(),
+ isPaused = player.paused(),
+ selectedSource;
+
+ player.addClass(QUALITY_CHANGE_CLASS);
+
+ // Find and set the new selected source
+ sources = _.map(sources, _.partial(_.omit, _, 'selected'));
+ selectedSource = _.findWhere(sources, { src: newSource.src });
+ // Note: `_.findWhere` returns a reference to an object. Thus the
+ // following updates the original object in `sources`.
+ selectedSource.selected = true;
+
+ player.src(sources);
+
+ player.one('loadeddata', function() {
+ player.removeClass(QUALITY_CHANGE_CLASS);
+ player.currentTime(currentTime);
+ if (!isPaused) {
+ player.play();
+ }
+ });
+ });
+
+ return {
+
+ setSource: function(playerSelectedSource, next) {
+ var sources = player.currentSources(),
+ userSelectedSource, chosenSource,
+ qualitySelector;
+
+ // There are generally two source options, the one that videojs
+ // auto-selects and the one that a "user" of this plugin has
+ // supplied via the `selected` property. `selected` can come from
+ // either the `