├── .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 | [](https://travis-ci.org/silvermine/videojs-quality-selector)
4 | [](https://coveralls.io/github/silvermine/videojs-quality-selector?branch=master)
5 | [](https://david-dm.org/silvermine/videojs-quality-selector)
6 | [](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 |
--------------------------------------------------------------------------------