├── .browserslistrc ├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .markdownlint.json ├── .npmignore ├── .nvmrc ├── .nycrc.json ├── .stylelintrc.yml ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── commitlint.config.js ├── docs └── demo │ └── index.html ├── package-lock.json ├── package.json ├── src ├── .eslintrc.json ├── js │ ├── components │ │ ├── QualityOption.js │ │ └── QualitySelector.js │ ├── events.js │ ├── index.js │ ├── middleware │ │ └── SourceInterceptor.js │ ├── standalone.js │ └── util │ │ └── SafeSeek.js └── scss │ └── quality-selector.scss └── tests ├── .eslintrc.json └── Placeholder.test.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | ./node_modules/@silvermine/standardization/browserslist/.browserslistrc-broad-support -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | node_modules/@silvermine/standardization/.editorconfig -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@silvermine/eslint-config/node", 3 | "parser": "babel-eslint" 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | outputs: 9 | nvmrc: ${{ steps.makeNodeVersionOutput.outputs.nvmrc }} 10 | steps: 11 | - 12 | uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 # Fetch all history 15 | - 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version-file: '.nvmrc' 19 | - 20 | name: Put NVM version in output 21 | id: makeNodeVersionOutput 22 | run: echo "nvmrc=$(cat .nvmrc)" >> "$GITHUB_OUTPUT" 23 | - run: npm ci 24 | - run: npm run check-node-version 25 | - run: npm run standards 26 | - run: npm run build 27 | - 28 | name: Check for uncommitted changes # Done after dependency install and build to ensure code isn't compromised 29 | run: if [ -n "$(git status --porcelain)" ]; then echo 'There are uncommitted changes.'; exit 1; fi 30 | test: 31 | needs: [ build ] 32 | runs-on: ubuntu-latest 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | node-version: [ 16, '${{ needs.build.outputs.nvmrc }}', 'lts/*', 'latest' ] 37 | steps: 38 | - 39 | uses: actions/checkout@v4 40 | with: 41 | fetch-depth: 0 # Fetch all history 42 | - 43 | name: Use Node.js ${{ matrix.node-version }} 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: ${{ matrix.node-version }} 47 | - run: npm ci # Reinstall the dependencies to ensure they install with the current version of node 48 | - run: npm run standards 49 | - run: npm run build # Ensure building is possible with this version of node 50 | - run: npm test 51 | - 52 | name: Coveralls 53 | uses: coverallsapp/github-action@v2 54 | with: 55 | parallel: true 56 | flag-name: ${{ matrix.node-version }} 57 | finish: 58 | needs: [ test ] 59 | runs-on: ubuntu-latest 60 | steps: 61 | - 62 | name: Close parallel build 63 | uses: coverallsapp/github-action@v2 64 | with: 65 | parallel-finished: true 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | coverage 4 | .nyc_output 5 | dist 6 | .idea 7 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@silvermine/standardization/.markdownlint.json" 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintrc.json 2 | .travis.yml 3 | Gruntfile.js 4 | tests/** 5 | docs 6 | .nyc_output 7 | coverage 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.12.2 2 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*.js" 4 | ], 5 | "extension": [ 6 | ".js" 7 | ], 8 | "reporter": [ 9 | "text-summary", 10 | "html", 11 | "lcov" 12 | ], 13 | "instrument": true, 14 | "sourceMap": true, 15 | "all": true 16 | } 17 | -------------------------------------------------------------------------------- /.stylelintrc.yml: -------------------------------------------------------------------------------- 1 | extends: ./node_modules/@silvermine/standardization/.stylelintrc.yml 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [our coding standards][commit-messages] for commit guidelines. 5 | 6 | ### [1.3.1](https://github.com/silvermine/videojs-quality-selector/compare/v1.3.0...v1.3.1) (2023-11-15) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * use correct icon for Video.js 8 ([1209756](https://github.com/silvermine/videojs-quality-selector/commit/1209756616af52843f55ac53e2c7fbe29df63541)) 12 | 13 | 14 | ## 1.2.3 15 | 16 | * Downgraded the `class.extend` dependency to 0.9.1. Version 0.9.2 introduces a call to 17 | `new Function(someString)`, which [violates the Content Security Policy that blocks 18 | `eval` and `eval`-like function 19 | calls.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_eval_expressions) 20 | (f9ca724 Fixes #36) 21 | * Fixed a bug where the quality selection menu did not render when sources were set 22 | sometime after the player was initially created and became ready (a3753dd Fixes #47). 23 | * Support 'selected' as a value for the `selected` attribute on `source` tags (8702f4f 24 | Fixes #39) 25 | 26 | ## 1.2.2 27 | 28 | * Fixed a bug introduced in `1.2.0` where the quality selector menu did not show the 29 | selected source as selected when it first rendered 30 | 31 | ## 1.2.1 32 | 33 | * Fixed a bug introduced in 31a305d where the path to the built JS file in the `dist` 34 | folder changed unintentionally 35 | * Fixed a bug that prevented the quality selector menu from fading out smoothly in 36 | Video.js 7. 37 | * Included Video.js 7 in peer dependency range (21900e8 Fixes #26) 38 | 39 | ## 1.2.0 40 | 41 | * Migrated NPM package to use `@silvermine` scope 42 | 43 | ## 1.1.2 44 | 45 | * Fixed a bug where selecting a quality menu item while a video was playing did not resume 46 | playback after the source changed. Affected Safari and players whose `preload` attribute 47 | was `none` (8feeafb Fixes #16). 48 | 49 | ## 1.1.1 50 | 51 | * Reference underscore as a dependency since we depend on it (931d8a4 See #12) 52 | 53 | ## 1.1.0 54 | 55 | **NOTE:** Strictly speaking, this version breaks API backwards-compatibility, and thus 56 | should have been a 2.0.0 release. However, the break in API was just the changing of an 57 | event name, and the event was not a documented event intended for external users to use 58 | (although they could have easily done so). Also, even if someone was using the event, 59 | depending on the specific reason they were using it, they may not need to make a change at 60 | all. 61 | 62 | If you were relying on the `QUALITY_SELECTED` event, it's possible that you will now need 63 | to rely on the `QUALITY_REQUESTED` event instead, depending on why you were listening to 64 | the event. See a682125 for details. 65 | 66 | * Support quality selector buttons anywhere in the player's component hierarchy (a682125 Fixes #13) 67 | 68 | ## 1.0.3 69 | 70 | * Stopped modifying format of passed-in source list (See 7da6fd3) 71 | 72 | ## 1.0.2 73 | 74 | * Added localization (cc7f670 fixes #7) 75 | 76 | ## 1.0.1 77 | 78 | * Fixed bug with binding to `QUALITY_SELECTED` way too many times (9dd9ca1 Fixes #5) 79 | 80 | ## 1.0.0 81 | 82 | * Added documentation and released the initial release of the plugin. 83 | 84 | ## 0.9.0 85 | 86 | * Working version of the plugin, as yet undocumented. 87 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 Jeremy Thomerson 3 | * Licensed under the MIT license. 4 | */ 5 | var path = require('path'), 6 | getCodeVersion = require('silvermine-serverless-utils/src/get-code-version'); 7 | 8 | const sass = require('sass'); 9 | 10 | module.exports = function(grunt) { 11 | 12 | var DEBUG = !!grunt.option('debug'), 13 | pkgJSON = grunt.file.readJSON('package.json'), 14 | config, versionInfo; 15 | 16 | try { 17 | versionInfo = getCodeVersion.both(); 18 | } catch(e) { 19 | // When this package is installed as a git URL, getCodeVersion throws an error and 20 | // is not able to find the git version for this package. So, we fall back to using 21 | // the version number from package.json 22 | versionInfo = pkgJSON.version; 23 | } 24 | 25 | config = { 26 | js: { 27 | all: [ 'Gruntfile.js', 'src/**/*.js', 'tests/**/*.js' ], 28 | standalone: path.join(__dirname, 'src', 'js', 'standalone.js'), 29 | }, 30 | 31 | sass: { 32 | base: path.join(__dirname, 'src', 'scss'), 33 | all: [ 'src/**/*.scss' ], 34 | }, 35 | 36 | dist: { 37 | base: path.join(__dirname, 'dist'), 38 | jsFileName: 'silvermine-videojs-quality-selector', 39 | }, 40 | }; 41 | 42 | config.dist.js = { 43 | bundle: path.join(config.dist.base, 'js', '<%= config.dist.jsFileName %>.js'), 44 | minified: path.join(config.dist.base, 'js', '<%= config.dist.jsFileName %>.min.js'), 45 | }; 46 | 47 | config.dist.css = { 48 | base: path.join(config.dist.base, 'css'), 49 | all: path.join(config.dist.base, '**', '*.css'), 50 | }; 51 | 52 | grunt.initConfig({ 53 | 54 | pkg: pkgJSON, 55 | versionInfo: versionInfo, 56 | config: config, 57 | 58 | browserify: { 59 | main: { 60 | src: config.js.standalone, 61 | dest: config.dist.js.bundle, 62 | options: { 63 | transform: [ 64 | [ 65 | 'babelify', 66 | { 67 | presets: [ 68 | [ 69 | '@babel/preset-env', 70 | { 71 | debug: true, 72 | useBuiltIns: 'usage', 73 | shippedProposals: true, 74 | corejs: 3, 75 | }, 76 | ], 77 | ], 78 | }, 79 | ], 80 | ], 81 | }, 82 | }, 83 | }, 84 | 85 | uglify: { 86 | main: { 87 | files: { 88 | '<%= config.dist.js.minified %>': config.dist.js.bundle, 89 | }, 90 | options: { 91 | banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> <%= versionInfo %> */\n', 92 | sourceMap: DEBUG, 93 | sourceMapIncludeSources: DEBUG, 94 | mangle: !DEBUG, 95 | // Disable the `merge_vars` option in the compression phase. 96 | // `merge_vars` aggressively reuses variable names, which can lead to 97 | // unexpected behavior or runtime errors in certain cases. 98 | compress: DEBUG ? false : { merge_vars: false }, // eslint-disable-line camelcase 99 | beautify: DEBUG, 100 | }, 101 | }, 102 | }, 103 | 104 | sass: { 105 | options: { 106 | implementation: sass, 107 | sourceMap: DEBUG, 108 | indentWidth: 3, 109 | outputStyle: DEBUG ? 'expanded' : 'compressed', 110 | sourceComments: DEBUG, 111 | }, 112 | main: { 113 | files: [ 114 | { 115 | expand: true, 116 | cwd: config.sass.base, 117 | src: [ '**/*.scss' ], 118 | dest: config.dist.css.base, 119 | ext: '.css', 120 | extDot: 'first', 121 | }, 122 | ], 123 | }, 124 | }, 125 | 126 | postcss: { 127 | options: { 128 | map: DEBUG, 129 | processors: [ 130 | require('autoprefixer')({ browsers: '> .05%' }), // eslint-disable-line global-require 131 | ], 132 | }, 133 | main: { 134 | src: config.dist.css.all, 135 | }, 136 | }, 137 | 138 | clean: { 139 | dist: config.dist.base, 140 | }, 141 | 142 | watch: { 143 | grunt: { 144 | files: [ 'Gruntfile.js' ], 145 | tasks: [ 'build' ], 146 | }, 147 | 148 | js: { 149 | files: [ 'src/**/*.js' ], 150 | tasks: [ 'build-js' ], 151 | }, 152 | 153 | css: { 154 | files: [ 'src/**/*.scss' ], 155 | tasks: [ 'build-css' ], 156 | }, 157 | }, 158 | }); 159 | 160 | grunt.loadNpmTasks('grunt-contrib-uglify'); 161 | grunt.loadNpmTasks('grunt-contrib-watch'); 162 | grunt.loadNpmTasks('grunt-browserify'); 163 | grunt.loadNpmTasks('grunt-postcss'); 164 | grunt.loadNpmTasks('grunt-contrib-clean'); 165 | grunt.loadNpmTasks('grunt-sass'); 166 | 167 | grunt.registerTask('build-js', [ 'browserify', 'uglify' ]); 168 | grunt.registerTask('build-css', [ 'sass', 'postcss' ]); 169 | grunt.registerTask('build', [ 'build-js', 'build-css' ]); 170 | grunt.registerTask('develop', [ 'build', 'watch' ]); 171 | grunt.registerTask('default', [ 'standards' ]); 172 | 173 | }; 174 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Jeremy Thomerson 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Silvermine VideoJS Quality/Resolution Selector 2 | 3 | [![Build Status](https://travis-ci.org/silvermine/videojs-quality-selector.svg?branch=master)](https://travis-ci.org/silvermine/videojs-quality-selector) 4 | [![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) 5 | [![Dependency Status](https://david-dm.org/silvermine/videojs-quality-selector.svg)](https://david-dm.org/silvermine/videojs-quality-selector) 6 | [![Dev Dependency Status](https://david-dm.org/silvermine/videojs-quality-selector/dev-status.svg)](https://david-dm.org/silvermine/videojs-quality-selector?type=dev) 7 | 8 | 9 | ## What is it? 10 | 11 | A plugin for [videojs](http://videojs.com/) versions 6+ that adds a button to the control 12 | bar which will allow the user to choose from available video qualities or resolutions. 13 | 14 | 15 | ## How do I use it? 16 | 17 | There are three primary steps to use this plug-in: [(1) including](#includingrequiring), 18 | [(2) providing sources](#providing-video-sources), and [(3) adding the component the to 19 | `controlBar`](#adding-to-the-player). Please see the following for information on each 20 | step. 21 | 22 | ### Including/Requiring 23 | 24 | #### Using ` 38 | 39 | ``` 40 | 41 | ##### From [`unpkg`](https://unpkg.com/@silvermine/videojs-quality-selector/) 42 | 43 | ```js 44 | 45 | 46 | 47 | ``` 48 | 49 | #### Using `require` 50 | 51 | When using NPM/Browserify, first install the plugin. 52 | 53 | ```bash 54 | npm install --save @silvermine/videojs-quality-selector 55 | ``` 56 | 57 | For `videojs` to use the plug-in, the plugin needs to register itself with the instance of 58 | `videojs`. This can be accomplished by: 59 | 60 | ```js 61 | var videojs = require('videojs'); 62 | 63 | // The following registers the plugin with `videojs` 64 | require('@silvermine/videojs-quality-selector')(videojs); 65 | ``` 66 | 67 | Remember to also add the CSS to your build. With most bundlers you can: 68 | 69 | ```js 70 | require('@silvermine/videojs-quality-selector/dist/css/quality-selector.css') 71 | ``` 72 | 73 | > [!WARNING] 74 | > This plugin's source code uses ES6+ syntax and keywords, such as `class` and `static`. 75 | > If you need to support [browsers that do not support newer JavaScript 76 | > syntax](https://caniuse.com/es6), you will need to use a tool like 77 | > [Babel](https://babeljs.io/) to transpile and polyfill your code. 78 | > 79 | > Alternatively, you can 80 | > `require('@silvermine/videojs-quality-selector/dist/js/silvermine-videojs-quality-selector.js')` 81 | > to use a JavaScript file that has already been polyfilled/transpiled down to ES5 82 | > compatibility. 83 | 84 | ### Providing video sources 85 | 86 | Sources can be provided with either the `` tag or via the `src` function on the 87 | instance of a `video.js` player. 88 | 89 | #### Using `` 90 | 91 | ```html 92 | 97 | ``` 98 | 99 | #### Using `player.src()` 100 | 101 | ```js 102 | player.src([ 103 | { 104 | src: 'https://example.com/video_720.mp4', 105 | type: 'video/mp4', 106 | label: '720P', 107 | }, 108 | { 109 | src: 'https://example.com/video_480.mp4', 110 | type: 'video/mp4', 111 | label: '480P', 112 | selected: true, 113 | }, 114 | { 115 | src: 'https://example.com/video_360.mp4', 116 | type: 'video/mp4', 117 | label: '360P', 118 | }, 119 | ]); 120 | ``` 121 | 122 | ### Adding to the player 123 | 124 | There are at least two ways to add the quality selector control to the player's control 125 | bar. The first is directly adding it via `addChild`. For example: 126 | 127 | ```js 128 | videojs('video_1', {}, function() { 129 | var player = this; 130 | 131 | player.controlBar.addChild('QualitySelector'); 132 | }); 133 | ``` 134 | 135 | The second option is to add the control via the player's options, for instance: 136 | 137 | ```js 138 | var options, player; 139 | 140 | options = { 141 | controlBar: { 142 | children: [ 143 | 'playToggle', 144 | 'progressControl', 145 | 'volumePanel', 146 | 'qualitySelector', 147 | 'fullscreenToggle', 148 | ], 149 | }, 150 | }; 151 | 152 | player = videojs('video_1', options); 153 | ``` 154 | 155 | ## How do I contribute? 156 | 157 | We genuinely appreciate external contributions. See [our extensive 158 | documentation](https://github.com/silvermine/silvermine-info#contributing) on how to 159 | contribute. 160 | 161 | 162 | ## License 163 | 164 | This software is released under the MIT license. See [the license file](LICENSE) for more 165 | details. 166 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ '@silvermine/standardization/commitlint.js' ], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | videojs-quality-selector Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Demo of videojs-quality-selector

13 | 14 | 20 | 21 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@silvermine/videojs-quality-selector", 3 | "version": "1.3.1", 4 | "description": "video.js plugin for selecting a video quality or resolution", 5 | "main": "src/js/index.js", 6 | "scripts": { 7 | "prepare": "npm run build", 8 | "check-node-version": "check-node-version --node $(cat .nvmrc) --npm 10.5.0 --print", 9 | "test": "nyc mocha -- 'tests/**/*.test.js'", 10 | "build": "grunt build", 11 | "build:debug": "grunt build --debug", 12 | "commitlint": "commitlint --from ad805e8", 13 | "markdownlint": "markdownlint -c .markdownlint.json -i CHANGELOG.md '{,!(node_modules)/**/}*.md'", 14 | "eslint": "eslint '{,!(node_modules|dist)/**/}*.js'", 15 | "stylelint": "stylelint './src/scss/**/*.scss'", 16 | "standards": "npm run commitlint && npm run markdownlint && npm run stylelint && npm run eslint", 17 | "release:preview": "node ./node_modules/@silvermine/standardization/scripts/release.js preview", 18 | "release:prep-changelog": "node ./node_modules/@silvermine/standardization/scripts/release.js prep-changelog", 19 | "release:finalize": "node ./node_modules/@silvermine/standardization/scripts/release.js finalize" 20 | }, 21 | "author": "Jeremy Thomerson", 22 | "license": "MIT", 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/silvermine/videojs-quality-selector.git" 26 | }, 27 | "keywords": [ 28 | "video.js", 29 | "videojs", 30 | "plugin", 31 | "resolution", 32 | "quality" 33 | ], 34 | "bugs": { 35 | "url": "https://github.com/silvermine/videojs-quality-selector/issues" 36 | }, 37 | "homepage": "https://github.com/silvermine/videojs-quality-selector#readme", 38 | "devDependencies": { 39 | "@babel/core": "7.13.16", 40 | "@babel/preset-env": "7.13.15", 41 | "@commitlint/cli": "8.3.5", 42 | "@commitlint/travis-cli": "8.3.5", 43 | "@silvermine/eslint-config": "3.0.1", 44 | "@silvermine/standardization": "2.0.0", 45 | "autoprefixer": "8.6.5", 46 | "babel-eslint": "10.1.0", 47 | "babelify": "10.0.0", 48 | "check-node-version": "4.0.3", 49 | "core-js": "3.11.0", 50 | "coveralls": "3.0.3", 51 | "eslint": "6.8.0", 52 | "expect.js": "0.3.1", 53 | "grunt": "1.4.0", 54 | "grunt-browserify": "5.3.0", 55 | "grunt-cli": "1.3.2", 56 | "grunt-contrib-clean": "2.0.0", 57 | "grunt-contrib-uglify": "5.2.2", 58 | "grunt-contrib-watch": "1.1.0", 59 | "grunt-eslint": "22.0.0", 60 | "grunt-markdownlint": "3.1.4", 61 | "grunt-postcss": "0.9.0", 62 | "grunt-sass": "3.0.2", 63 | "grunt-stylelint": "0.16.0", 64 | "mocha": "8.4.0", 65 | "mocha-lcov-reporter": "1.3.0", 66 | "nyc": "15.1.0", 67 | "rewire": "4.0.1", 68 | "sass": "1.49.7", 69 | "silvermine-serverless-utils": "git+https://github.com/silvermine/serverless-utils.git#910f1149af824fc8d0fa840878079c7d3df0f414", 70 | "sinon": "7.3.2" 71 | }, 72 | "peerDependencies": { 73 | "video.js": ">=6.0.0" 74 | }, 75 | "dependencies": { 76 | "underscore": "1.13.1" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "extends": "@silvermine/eslint-config/browser" 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/js/components/QualityOption.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | events = require('../events'); 3 | 4 | module.exports = function(videojs) { 5 | var MenuItem = videojs.getComponent('MenuItem'); 6 | 7 | /** 8 | * A MenuItem to represent a video resolution 9 | * 10 | * @class QualityOption 11 | * @extends videojs.MenuItem 12 | */ 13 | return class QualityOption extends MenuItem { 14 | 15 | /** 16 | * @inheritdoc 17 | */ 18 | constructor(player, options) { 19 | var source = options.source; 20 | 21 | if (!_.isObject(source)) { 22 | throw new Error('was not provided a "source" object, but rather: ' + (typeof source)); 23 | } 24 | 25 | options = _.extend({ 26 | selectable: true, 27 | label: source.label, 28 | }, options); 29 | 30 | super(player, options); 31 | 32 | this.source = source; 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | */ 38 | handleClick(event) { 39 | super.handleClick(event); 40 | this.player().trigger(events.QUALITY_REQUESTED, this.source); 41 | } 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/js/components/QualitySelector.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | events = require('../events'), 3 | qualityOptionFactory = require('./QualityOption'), 4 | QUALITY_CHANGE_CLASS = 'vjs-quality-changing'; 5 | 6 | module.exports = function(videojs) { 7 | var MenuButton = videojs.getComponent('MenuButton'), 8 | QualityOption = qualityOptionFactory(videojs); 9 | 10 | /** 11 | * A component for changing video resolutions 12 | * 13 | * @class QualitySelector 14 | * @extends videojs.Button 15 | */ 16 | class QualitySelector extends MenuButton { 17 | 18 | /** 19 | * @inheritdoc 20 | */ 21 | constructor(player, options) { 22 | super(player, options); 23 | 24 | // Update interface instantly so the user's change is acknowledged 25 | player.on(events.QUALITY_REQUESTED, function(event, newSource) { 26 | this.setSelectedSource(newSource); 27 | player.addClass(QUALITY_CHANGE_CLASS); 28 | 29 | player.one('loadeddata', function() { 30 | player.removeClass(QUALITY_CHANGE_CLASS); 31 | }); 32 | }.bind(this)); 33 | 34 | // Update the list of menu items only when the list of sources change 35 | player.on(events.PLAYER_SOURCES_CHANGED, function() { 36 | this.update(); 37 | }.bind(this)); 38 | 39 | player.on(events.QUALITY_SELECTED, function(event, newSource) { 40 | // Update the selected source with the source that was actually selected 41 | this.setSelectedSource(newSource); 42 | }.bind(this)); 43 | 44 | // Since it's possible for the player to get a source before the selector is 45 | // created, make sure to update once we get a "ready" signal. 46 | player.one('ready', function() { 47 | this.selectedSrc = player.src(); 48 | this.update(); 49 | }.bind(this)); 50 | 51 | this.controlText('Open quality selector menu'); 52 | } 53 | 54 | /** 55 | * Updates the source that is selected in the menu 56 | * 57 | * @param source {object} player source to display as selected 58 | */ 59 | setSelectedSource(source) { 60 | var src = (source ? source.src : undefined); 61 | 62 | if (this.selectedSrc !== src) { 63 | this.selectedSrc = src; 64 | _.each(this.items, function(item) { 65 | item.selected(item.source.src === src); 66 | }); 67 | } 68 | } 69 | 70 | /** 71 | * @inheritdoc 72 | */ 73 | createItems() { 74 | var player = this.player(), 75 | sources = player.currentSources(); 76 | 77 | if (!sources || sources.length < 2) { 78 | return []; 79 | } 80 | 81 | return _.map(sources, function(source) { 82 | return new QualityOption(player, { 83 | source: source, 84 | selected: source.src === this.selectedSrc, 85 | }); 86 | }.bind(this)); 87 | } 88 | 89 | /** 90 | * @inheritdoc 91 | */ 92 | buildWrapperCSSClass() { 93 | return 'vjs-quality-selector ' + super.buildWrapperCSSClass(); 94 | } 95 | } 96 | 97 | videojs.registerComponent('QualitySelector', QualitySelector); 98 | 99 | return QualitySelector; 100 | }; 101 | -------------------------------------------------------------------------------- /src/js/events.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | QUALITY_REQUESTED: 'qualityRequested', 3 | QUALITY_SELECTED: 'qualitySelected', 4 | PLAYER_SOURCES_CHANGED: 'playerSourcesChanged', 5 | }; 6 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | events = require('./events'), 3 | qualitySelectorFactory = require('./components/QualitySelector'), 4 | sourceInterceptorFactory = require('./middleware/SourceInterceptor'), 5 | SafeSeek = require('./util/SafeSeek'); 6 | 7 | module.exports = function(videojs) { 8 | videojs = videojs || window.videojs; 9 | 10 | qualitySelectorFactory(videojs); 11 | sourceInterceptorFactory(videojs); 12 | 13 | videojs.hook('setup', function(player) { 14 | function changeQuality(event, newSource) { 15 | var sources = player.currentSources(), 16 | currentTime = player.currentTime(), 17 | currentPlaybackRate = player.playbackRate(), 18 | isPaused = player.paused(), 19 | selectedSource; 20 | 21 | // Clear out any previously selected sources (see: #11) 22 | _.each(sources, function(source) { 23 | source.selected = false; 24 | }); 25 | 26 | selectedSource = _.findWhere(sources, { src: newSource.src }); 27 | // Note: `_.findWhere` returns a reference to an object. Thus the 28 | // following updates the original object in `sources`. 29 | selectedSource.selected = true; 30 | 31 | if (player._qualitySelectorSafeSeek) { 32 | player._qualitySelectorSafeSeek.onQualitySelectionChange(); 33 | } 34 | 35 | player.src(sources); 36 | 37 | player.ready(function() { 38 | if (!player._qualitySelectorSafeSeek || player._qualitySelectorSafeSeek.hasFinished()) { 39 | // Either we don't have a pending seek action or the one that we have is no 40 | // longer applicable. This block must be within a `player.ready` callback 41 | // because the call to `player.src` above is asynchronous, and so not 42 | // having it within this `ready` callback would cause the SourceInterceptor 43 | // to execute after this block instead of before. 44 | // 45 | // We save the `currentTime` within the SafeSeek instance because if 46 | // multiple QUALITY_REQUESTED events are received before the SafeSeek 47 | // operation finishes, the player's `currentTime` will be `0` if the 48 | // player's `src` is updated but the player's `currentTime` has not yet 49 | // been set by the SafeSeek operation. 50 | player._qualitySelectorSafeSeek = new SafeSeek(player, currentTime); 51 | player.playbackRate(currentPlaybackRate); 52 | } 53 | 54 | if (!isPaused) { 55 | player.play(); 56 | } 57 | }); 58 | } 59 | 60 | // Add handler to switch sources when the user requests a change 61 | player.on(events.QUALITY_REQUESTED, changeQuality); 62 | }); 63 | }; 64 | 65 | module.exports.EVENTS = events; 66 | -------------------------------------------------------------------------------- /src/js/middleware/SourceInterceptor.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | events = require('../events'); 3 | 4 | module.exports = function(videojs) { 5 | 6 | videojs.use('*', function(player) { 7 | 8 | return { 9 | 10 | setSource: function(playerSelectedSource, next) { 11 | var sources = player.currentSources(), 12 | userSelectedSource, chosenSource; 13 | 14 | if (player._qualitySelectorSafeSeek) { 15 | player._qualitySelectorSafeSeek.onPlayerSourcesChange(); 16 | } 17 | 18 | if (!_.isEqual(sources, player._qualitySelectorPreviousSources)) { 19 | player.trigger(events.PLAYER_SOURCES_CHANGED, sources); 20 | player._qualitySelectorPreviousSources = sources; 21 | } 22 | 23 | // There are generally two source options, the one that videojs 24 | // auto-selects and the one that a "user" of this plugin has 25 | // supplied via the `selected` property. `selected` can come from 26 | // either the `` tag or the list of sources passed to 27 | // videojs using `src()`. 28 | 29 | userSelectedSource = _.find(sources, function(source) { 30 | // Must check for boolean values as well as either the string 'true' or 31 | // 'selected'. When sources are set programmatically, the value will be a 32 | // boolean, but those coming from a `` tag will be a string. 33 | return source.selected === true || source.selected === 'true' || source.selected === 'selected'; 34 | }); 35 | 36 | chosenSource = userSelectedSource || playerSelectedSource; 37 | 38 | player.trigger(events.QUALITY_SELECTED, chosenSource); 39 | 40 | // Pass along the chosen source 41 | next(null, chosenSource); 42 | }, 43 | 44 | }; 45 | 46 | }); 47 | 48 | }; 49 | -------------------------------------------------------------------------------- /src/js/standalone.js: -------------------------------------------------------------------------------- 1 | require('./index')(); 2 | -------------------------------------------------------------------------------- /src/js/util/SafeSeek.js: -------------------------------------------------------------------------------- 1 | class SafeSeek { 2 | constructor(player, seekToTime) { 3 | this._player = player; 4 | this._seekToTime = seekToTime; 5 | this._hasFinished = false; 6 | this._keepThisInstanceWhenPlayerSourcesChange = false; 7 | this._seekWhenSafe(); 8 | } 9 | 10 | _seekWhenSafe() { 11 | var HAVE_FUTURE_DATA = 3; 12 | 13 | // `readyState` in Video.js is the same as the HTML5 Media element's `readyState` 14 | // property. 15 | // 16 | // `readyState` is an enum of 5 values (0-4), each of which represent a state of 17 | // readiness to play. The meaning of the values range from HAVE_NOTHING (0), meaning 18 | // no data is available to HAVE_ENOUGH_DATA (4), meaning all data is loaded and the 19 | // video can be played all the way through. 20 | // 21 | // In order to seek successfully, the `readyState` must be at least HAVE_FUTURE_DATA 22 | // (3). 23 | // 24 | // @see http://docs.videojs.com/player#readyState 25 | // @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState 26 | // @see https://dev.w3.org/html5/spec-preview/media-elements.html#seek-the-media-controller 27 | if (this._player.readyState() < HAVE_FUTURE_DATA) { 28 | this._seekFn = this._seek.bind(this); 29 | // The `canplay` event means that the `readyState` is at least HAVE_FUTURE_DATA. 30 | this._player.one('canplay', this._seekFn); 31 | } else { 32 | this._seek(); 33 | } 34 | } 35 | 36 | onPlayerSourcesChange() { 37 | if (this._keepThisInstanceWhenPlayerSourcesChange) { 38 | // By setting this to `false`, we know that if the player sources change again 39 | // the change did not originate from a quality selection change, the new sources 40 | // are likely different from the old sources, and so this pending seek no longer 41 | // applies. 42 | this._keepThisInstanceWhenPlayerSourcesChange = false; 43 | } else { 44 | this.cancel(); 45 | } 46 | } 47 | 48 | onQualitySelectionChange() { 49 | // `onPlayerSourcesChange` will cancel this pending seek unless we tell it not to. 50 | // We need to reuse this same pending seek instance because when the player is 51 | // paused, the `preload` attribute is set to `none`, and the user selects one 52 | // quality option and then another, the player cannot seek until the player has 53 | // enough data to do so (and the `canplay` event is fired) and thus on the second 54 | // selection the player's `currentTime()` is `0` and when the video plays we would 55 | // seek to `0` instead of the correct time. 56 | if (!this.hasFinished()) { 57 | this._keepThisInstanceWhenPlayerSourcesChange = true; 58 | } 59 | } 60 | 61 | _seek() { 62 | this._player.currentTime(this._seekToTime); 63 | this._keepThisInstanceWhenPlayerSourcesChange = false; 64 | this._hasFinished = true; 65 | } 66 | 67 | hasFinished() { 68 | return this._hasFinished; 69 | } 70 | 71 | cancel() { 72 | this._player.off('canplay', this._seekFn); 73 | this._keepThisInstanceWhenPlayerSourcesChange = false; 74 | this._hasFinished = true; 75 | } 76 | } 77 | 78 | module.exports = SafeSeek; 79 | -------------------------------------------------------------------------------- /src/scss/quality-selector.scss: -------------------------------------------------------------------------------- 1 | .vjs-quality-selector { 2 | .vjs-menu-button { 3 | margin: 0; 4 | padding: 0; 5 | height: 100%; 6 | width: 100%; 7 | } 8 | .vjs-icon-placeholder { 9 | // From video.js font: https://github.com/videojs/font 10 | /* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */ 11 | font-family: 'VideoJS'; 12 | font-weight: normal; 13 | font-style: normal; 14 | &::before { 15 | // The correct icon font character for Video.js 7 and below: 16 | .video-js:not(.vjs-v8) & { 17 | content: '\f110'; 18 | } 19 | // Icon font character for Video.js 8: 20 | .vjs-v8 & { 21 | content: '\f114'; 22 | } 23 | } 24 | } 25 | } 26 | 27 | .vjs-quality-changing { 28 | .vjs-big-play-button { 29 | display: none; 30 | } 31 | .vjs-control-bar { 32 | display: flex; 33 | visibility: visible; 34 | opacity: 1; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "extends": "@silvermine/eslint-config/node-tests" 4 | 5 | } 6 | -------------------------------------------------------------------------------- /tests/Placeholder.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | 3 | describe('Everything', function() { 4 | 5 | it('needs to be tested', function() { 6 | expect(true).to.be(true); 7 | }); 8 | 9 | }); 10 | --------------------------------------------------------------------------------