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..fc92718 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -5,29 +5,144 @@ '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('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..6979b87 --- /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..fb18187 --- /dev/null +++ b/src/js/components/QualitySelector.js @@ -0,0 +1,80 @@ +'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); + + this.selectedSource = options.selectedSource || player.currentSource(); + + 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.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.selectedSource = source; + 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: this.selectedSource ? source.src === this.selectedSource.src : false, + }); + }.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..c3bd072 --- /dev/null +++ b/src/js/middleware/SourceInterceptor.js @@ -0,0 +1,72 @@ +'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 + // Note: See `setSource` for the reason behind using both 'isDefault' + // and 'isdefault' + sources = _.map(sources, _.partial(_.omit, _, [ 'isDefault', 'isdefault' ])); + selectedSource = _.findWhere(sources, { src: newSource.src }); + // Note: `_.findWhere` returns a reference to an object. Thus the + // following updates the original object in `sources`. + selectedSource.isDefault = true; + + player.src(sources); + + player.one('loadeddata', function() { + player.removeClass(QUALITY_CHANGE_CLASS); + player.currentTime(currentTime); + if (!isPaused) { + player.play(); + } + }); + }); + + return { + + setSource: function(autoSelectedSource, next) { + var sources = player.currentSources(), + defaultSource, selectedSource, + qualitySelector; + + defaultSource = _.find(sources, function(source) { + // While the simplest check would be `!!source.isDefault`, remember that + // the sources can come from a `` tag. Therefore, the lowercase + // form, `isdefault`, needs to be checked. + return source.isDefault === true + || source.isDefault === 'true' + || source.isdefault === true + || source.isdefault === 'true'; + }); + + selectedSource = defaultSource || autoSelectedSource; + + // Update the quality selector with the new source + qualitySelector = player.controlBar.getChild('qualitySelector'); + if (qualitySelector) { + qualitySelector.update(); + } + + // Pass along selected source + next(null, selectedSource); + }, + + }; + + }); + +}; diff --git a/src/js/standalone.js b/src/js/standalone.js new file mode 100644 index 0000000..be70200 --- /dev/null +++ b/src/js/standalone.js @@ -0,0 +1,3 @@ +'use strict'; + +require('./index')(); diff --git a/src/sass/quality-selector.scss b/src/sass/quality-selector.scss new file mode 100644 index 0000000..b2762fd --- /dev/null +++ b/src/sass/quality-selector.scss @@ -0,0 +1,28 @@ +.vjs-quality-selector { + .vjs-menu-button { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + } + .vjs-icon-placeholder { + // From video.js font: https://github.com/videojs/font + font-family: 'VideoJS'; + font-weight: normal; + font-style: normal; + &:before { + content: '\f110'; + } + } +} + +.vjs-quality-changing { + .vjs-big-play-button { + display: none; + } + .vjs-control-bar { + display: flex; + visibility: visible; + opacity: 1; + } +}