From 156e5923c712e698fc5ed31dd776d403d78e9476 Mon Sep 17 00:00:00 2001 From: Ethan Smith Date: Thu, 22 Jun 2017 09:34:19 -0400 Subject: [PATCH 1/5] Initial implementation --- .gitignore | 1 + .npmignore | 1 + Gruntfile.js | 119 ++++++++++++++++++++++++- README.md | 6 +- docs/demo/index.html | 30 +++++++ package.json | 12 ++- src/index.js | 3 - src/js/components/QualityOption.js | 46 ++++++++++ src/js/components/QualitySelector.js | 80 +++++++++++++++++ src/js/events.js | 7 ++ src/js/index.js | 14 +++ src/js/middleware/SourceInterceptor.js | 72 +++++++++++++++ src/js/standalone.js | 3 + src/sass/quality-selector.scss | 28 ++++++ 14 files changed, 413 insertions(+), 9 deletions(-) create mode 100644 docs/demo/index.html delete mode 100644 src/index.js create mode 100644 src/js/components/QualityOption.js create mode 100644 src/js/components/QualitySelector.js create mode 100644 src/js/events.js create mode 100644 src/js/index.js create mode 100644 src/js/middleware/SourceInterceptor.js create mode 100644 src/js/standalone.js create mode 100644 src/sass/quality-selector.scss 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; + } +} From b4a9aa73d01ab4bb31b5650d3c5c33c8aa96e8ed Mon Sep 17 00:00:00 2001 From: Ethan Smith Date: Thu, 3 Aug 2017 15:21:24 -0400 Subject: [PATCH 2/5] Highlight correct resolution in the UI when the source is changed programmatically --- src/js/middleware/SourceInterceptor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/middleware/SourceInterceptor.js b/src/js/middleware/SourceInterceptor.js index c3bd072..67e8495 100644 --- a/src/js/middleware/SourceInterceptor.js +++ b/src/js/middleware/SourceInterceptor.js @@ -58,7 +58,7 @@ module.exports = function(videojs) { // Update the quality selector with the new source qualitySelector = player.controlBar.getChild('qualitySelector'); if (qualitySelector) { - qualitySelector.update(); + qualitySelector.setSelectedSource(selectedSource); } // Pass along selected source From 26da31607dc7d637c5f5d752b712743b952203c6 Mon Sep 17 00:00:00 2001 From: Ethan Smith Date: Fri, 4 Aug 2017 09:48:26 -0400 Subject: [PATCH 3/5] PR Modification: Use 'selected' instead of 'isDefault' --- docs/demo/index.html | 2 +- src/js/middleware/SourceInterceptor.js | 37 +++++++++++++------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/docs/demo/index.html b/docs/demo/index.html index 6979b87..7011c5c 100644 --- a/docs/demo/index.html +++ b/docs/demo/index.html @@ -13,7 +13,7 @@ diff --git a/src/js/middleware/SourceInterceptor.js b/src/js/middleware/SourceInterceptor.js index 67e8495..d2223ca 100644 --- a/src/js/middleware/SourceInterceptor.js +++ b/src/js/middleware/SourceInterceptor.js @@ -17,13 +17,11 @@ module.exports = function(videojs) { 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' ])); + 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.isDefault = true; + selectedSource.selected = true; player.src(sources); @@ -38,31 +36,34 @@ module.exports = function(videojs) { return { - setSource: function(autoSelectedSource, next) { + setSource: function(playerSelectedSource, next) { var sources = player.currentSources(), - defaultSource, selectedSource, + userSelectedSource, chosenSource, 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'; + // 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 `` tag or the list of sources passed to + // videojs using `src()`. + + userSelectedSource = _.find(sources, function(source) { + // Must check for both boolean and string 'true' as sources set + // programmatically should use a boolean, but those coming from + // a `` tag will use a string. + return source.selected === true || source.selected === 'true'; }); - selectedSource = defaultSource || autoSelectedSource; + chosenSource = userSelectedSource || playerSelectedSource; // Update the quality selector with the new source qualitySelector = player.controlBar.getChild('qualitySelector'); if (qualitySelector) { - qualitySelector.setSelectedSource(selectedSource); + qualitySelector.setSelectedSource(chosenSource); } - // Pass along selected source - next(null, selectedSource); + // Pass along the chosen source + next(null, chosenSource); }, }; From 449a0a54d784965a096e07851109a5f91d2b8fdc Mon Sep 17 00:00:00 2001 From: Ethan Smith Date: Fri, 4 Aug 2017 10:45:15 -0400 Subject: [PATCH 4/5] Ensure the correct resolution is selected on player 'ready' --- src/js/components/QualitySelector.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/js/components/QualitySelector.js b/src/js/components/QualitySelector.js index fb18187..72e6ac8 100644 --- a/src/js/components/QualitySelector.js +++ b/src/js/components/QualitySelector.js @@ -23,8 +23,6 @@ module.exports = function(videojs) { 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)); @@ -32,6 +30,7 @@ module.exports = function(videojs) { // 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)); }, @@ -42,7 +41,7 @@ module.exports = function(videojs) { * @param source {object} player source to display as selected */ setSelectedSource: function(source) { - this.selectedSource = source; + this.selectedSrc = source ? source.src : undefined; this.update(); }, @@ -60,7 +59,7 @@ module.exports = function(videojs) { return _.map(sources, function(source) { return new QualityOption(player, { source: source, - selected: this.selectedSource ? source.src === this.selectedSource.src : false, + selected: source.src === this.selectedSrc, }); }.bind(this)); }, From cc937f4f49d5a56aa10f594fdb47f9f831546aa2 Mon Sep 17 00:00:00 2001 From: Ethan Smith Date: Fri, 4 Aug 2017 14:23:15 -0400 Subject: [PATCH 5/5] Add 'grunt develop' --- Gruntfile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Gruntfile.js b/Gruntfile.js index fc92718..ac3fd8b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -143,6 +143,7 @@ module.exports = function(grunt) { 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' ]); };