├── .editorconfig ├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── example-2.png ├── example.png ├── index.html ├── lang ├── en.json └── fr.json ├── package-lock.json ├── package.json ├── scripts ├── karma.conf.js ├── postcss.config.js └── rollup.config.js ├── src ├── ConcreteButton.js ├── ConcreteMenuItem.js ├── plugin.css └── plugin.js └── test └── plugin.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | should-skip: 7 | continue-on-error: true 8 | runs-on: ubuntu-latest 9 | # Map a step output to a job output 10 | outputs: 11 | should-skip-job: ${{steps.skip-check.outputs.should_skip}} 12 | steps: 13 | - id: skip-check 14 | uses: fkirc/skip-duplicate-actions@v5.3.0 15 | with: 16 | github_token: ${{github.token}} 17 | 18 | ci: 19 | needs: should-skip 20 | if: ${{needs.should-skip.outputs.should-skip-job != 'true' || github.ref == 'refs/heads/main'}} 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | os: [ubuntu-latest] 25 | test-type: ['unit', 'coverage'] 26 | env: 27 | BROWSER_STACK_USERNAME: ${{secrets.BROWSER_STACK_USERNAME}} 28 | BROWSER_STACK_ACCESS_KEY: ${{secrets.BROWSER_STACK_ACCESS_KEY}} 29 | CI_TEST_TYPE: ${{matrix.test-type}} 30 | runs-on: ${{matrix.os}} 31 | steps: 32 | - name: checkout code 33 | uses: actions/checkout@v3 34 | 35 | - name: read node version from .nvmrc 36 | run: echo "NVMRC=$(cat .nvmrc)" >> $GITHUB_OUTPUT 37 | shell: bash 38 | id: nvm 39 | 40 | - name: update apt cache on linux w/o browserstack 41 | run: sudo apt-get update 42 | 43 | - name: install ffmpeg/pulseaudio for firefox on linux w/o browserstack 44 | run: sudo apt-get install ffmpeg pulseaudio 45 | 46 | - name: start pulseaudio for firefox on linux w/o browserstack 47 | run: pulseaudio -D 48 | 49 | - name: setup node 50 | uses: actions/setup-node@v3 51 | with: 52 | node-version: '${{steps.nvm.outputs.NVMRC}}' 53 | cache: npm 54 | 55 | # turn off the default setup-node problem watchers... 56 | - run: echo "::remove-matcher owner=eslint-compact::" 57 | - run: echo "::remove-matcher owner=eslint-stylish::" 58 | - run: echo "::remove-matcher owner=tsc::" 59 | 60 | - name: npm install 61 | run: npm i --prefer-offline --no-audit 62 | 63 | - name: run npm test 64 | uses: coactions/setup-xvfb@v1 65 | with: 66 | run: npm run test 67 | 68 | - name: coverage 69 | uses: codecov/codecov-action@v3 70 | with: 71 | token: ${{secrets.CODECOV_TOKEN}} 72 | files: './test/dist/coverage/coverage-final.json' 73 | fail_ci_if_error: true 74 | if: ${{startsWith(env.CI_TEST_TYPE, 'coverage')}} 75 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | # Setup .npmrc file to publish to npm 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: '18.x' 14 | registry-url: 'https://registry.npmjs.org' 15 | - run: npm ci 16 | - run: npm publish 17 | env: 18 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | Thumbs.db 3 | ehthumbs.db 4 | Desktop.ini 5 | .DS_Store 6 | ._* 7 | 8 | # Editors 9 | *~ 10 | *.swp 11 | *.tmproj 12 | *.tmproject 13 | *.sublime-* 14 | .idea/ 15 | .project/ 16 | .settings/ 17 | .vscode/ 18 | 19 | # Logs 20 | logs 21 | *.log 22 | npm-debug.log* 23 | 24 | # Dependency directories 25 | bower_components/ 26 | node_modules/ 27 | 28 | # Build-related directories 29 | dist/ 30 | es/ 31 | cjs/ 32 | docs/api/ 33 | test/dist/ 34 | .eslintcache 35 | .yo-rc.json 36 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Intentionally left blank, so that npm does not ignore anything by default, 2 | # but relies on the package.json "files" array to explicitly define what ends 3 | # up in the package. 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [2.0.0](https://github.com/chrisboustead/videojs-hls-quality-selector/compare/v1.3.0...v2.0.0) (2024-03-13) 3 | 4 | 5 | # [1.3.0](https://github.com/chrisboustead/videojs-hls-quality-selector/compare/v1.2.0...v1.3.0) (2024-03-13) 6 | 7 | ### Features 8 | 9 | * **gha:** publish action ([cd8e58a](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/cd8e58a)) 10 | 11 | ### Bug Fixes 12 | 13 | * **package:** update to node 18 ([631e973](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/631e973)) 14 | 15 | ### Chores 16 | 17 | * merge conflict ([0b1efc9](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/0b1efc9)) 18 | * merge conflict ([b067cba](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/b067cba)) 19 | * merge conflict ([20cfee8](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/20cfee8)) 20 | * **package:** npm update ([ee92237](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/ee92237)) 21 | 22 | ### Chores 23 | 24 | 25 | ## Upgrade template generator for 8.0 compatibility [1.2.0](https://github.com/chrisboustead/videojs-hls-quality-selector/compare/v1.1.4...v1.2.0) (2023-12-29) 26 | 27 | 28 | 29 | ## [1.1.4](https://github.com/chrisboustead/videojs-hls-quality-selector/compare/v1.1.3...v1.1.4) (2020-10-07) 30 | 31 | ### Bug Fixes 32 | 33 | * **npm:** remove force resolutions ([ab34990](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/ab34990)) 34 | 35 | 36 | ## [1.1.3](https://github.com/chrisboustead/videojs-hls-quality-selector/compare/v1.1.2...v1.1.3) (2020-10-07) 37 | 38 | ### Bug Fixes 39 | 40 | * **docs:** documentation formatting ([80d7bb3](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/80d7bb3)) 41 | * **plugin:** add a quality getter ([13b8be9](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/13b8be9)) 42 | 43 | 44 | ## [1.1.2](https://github.com/chrisboustead/videojs-hls-quality-selector/compare/v1.1.1...v1.1.2) (2020-09-03) 45 | 46 | ### Bug Fixes 47 | 48 | * **deps:** force update dot-prop ([4663a0c](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/4663a0c)) 49 | * **deps:** update deps ([c69e167](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/c69e167)) 50 | 51 | ### Chores 52 | 53 | * **deps:** bump lodash from 4.17.14 to 4.17.19 ([ecbcc28](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/ecbcc28)) 54 | 55 | 56 | ## [1.1.1](https://github.com/chrisboustead/videojs-hls-quality-selector/compare/v1.1.0...v1.1.1) (2020-01-17) 57 | 58 | ### Bug Fixes 59 | 60 | * **deps:** bump dependencies ([4980678](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/4980678)) 61 | * **plugin:** fix merged code lint issues ([dba70d5](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/dba70d5)) 62 | 63 | ### Chores 64 | 65 | * **deps:** bump lodash.template from 4.4.0 to 4.5.0 ([b559126](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/b559126)) 66 | 67 | 68 | # [1.1.0](https://github.com/chrisboustead/videojs-hls-quality-selector/compare/v1.0.5...v1.1.0) (2019-10-11) 69 | 70 | ### Features 71 | 72 | * **config:** display resolutions in menu button ([d285eef](https://github.com/chrisboustead/videojs-hls-quality-selector/commit/d285eef)) 73 | 74 | 75 | ## [0.0.8](https://github.com/chrisboustead/videojs-hls-quality-selector/compare/v0.0.7...v0.0.8) (2018-06-27) 76 | 77 | 78 | ## [0.0.7](https://github.com/chrisboustead/videojs-hls-quality-selector/compare/v0.0.6...v0.0.7) (2018-06-05) 79 | 80 | 81 | ## [0.0.6](https://github.com/chrisboustead/videojs-hls-quality-selector/compare/v0.0.5...v0.0.6) (2018-06-05) 82 | 83 | 84 | ## [0.0.5](https://github.com/chrisboustead/videojs-hls-quality-selector/compare/v0.0.4...v0.0.5) (2018-06-05) 85 | 86 | 87 | ## [0.0.4](https://github.com/chrisboustead/videojs-hls-quality-selector/compare/v0.0.3...v0.0.4) (2018-04-27) 88 | 89 | 90 | ## 0.0.3 (2018-04-27) 91 | 92 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | We welcome contributions from everyone! 4 | 5 | ## Getting Started 6 | 7 | Make sure you have Node.js 14 or higher and npm installed. 8 | 9 | 1. Fork this repository and clone your fork 10 | 1. Install dependencies: `npm install` 11 | 1. Run a development server: `npm start` 12 | 13 | ### Making Changes 14 | 15 | Refer to the [video.js plugin conventions][conventions] for more detail on best practices and tooling for video.js plugin authorship. 16 | 17 | When you've made your changes, push your commit(s) to your fork and issue a pull request against the original repository. 18 | 19 | ### Running Tests 20 | 21 | Testing is a crucial part of any software project. For all but the most trivial changes (typos, etc) test cases are expected. Tests are run in actual browsers using [Karma][karma]. 22 | 23 | - In all available and supported browsers: `npm test` 24 | - In a specific browser: `npm run test:chrome`, `npm run test:firefox`, etc. 25 | - While development server is running (`npm start`), navigate to [`http://localhost:9999/test/`][local] 26 | 27 | 28 | [karma]: http://karma-runner.github.io/ 29 | [local]: http://localhost:9999/test/ 30 | [conventions]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/conventions.md 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Chris Boustead (chris@forgemotion.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # videojs-hls-quality-selector 2 | [![CircleCI](https://circleci.com/gh/chrisboustead/videojs-hls-quality-selector/tree/master.svg?style=svg)](https://circleci.com/gh/chrisboustead/videojs-hls-quality-selector/tree/master) 3 | [![npm version](https://badge.fury.io/js/videojs-hls-quality-selector.svg)](https://badge.fury.io/js/videojs-hls-quality-selector) 4 | 5 | **Note:** 6 | - v1.2.0 is compatible with videojs 8 7 | - v1.x.x is Only compatible with VideoJS 7.x due to the move from `videojs-contrib-hls` to `videojs/http-streaming`. For VideoJS v5 or v6 support please use a `v0.x.x` tag 8 | 9 | ## Description 10 | 11 | Adds a quality selector menu for HLS sources played in videojs. 12 | 13 | Any HLS manifest with multiple playlists/renditions should be selectable from within the added control. 14 | 15 | **Native HLS** 16 | 17 | Does not yet support browsers using native HLS (Safari, Edge, etc). To enable plugin in browsers with native HLS, you must force non-native HLS playback: 18 | 19 | ## Options 20 | 21 | **displayCurrentQuality** `boolean` - _false_ 22 | 23 | Set to true to display the currently selected resolution in the menu button. When not enabled, displayed an included VJS "HD" icon. 24 | 25 | **placementIndex** `integer` 26 | 27 | Set this to override the default positioning of the menu button in the control bar relative to the other components in the control bar. 28 | 29 | **vjsIconClass** `string` - _"vjs-icon-hd"_ 30 | 31 | Set this to one of the custom VJS icons ([https://videojs.github.io/font/](https://videojs.github.io/font/)) to override the icon for the menu button. 32 | 33 | 34 | ## Methods 35 | 36 | **getCurrentQuality** `string` - _'auto'__ 37 | 38 | Return the current set quality or 'auto' 39 | 40 | 41 | ## Screenshots 42 | 43 | Default setup - Menu selected: 44 | ![Example](example.png) 45 | 46 | 47 | Display Current Quality option enabled: 48 | ![Example](example-2.png) 49 | 50 | ## Table of Contents 51 | 52 | 53 | 54 | ## Installation 55 | 56 | ```sh 57 | npm install --save videojs-hls-quality-selector 58 | ``` 59 | 60 | ## Usage 61 | 62 | To include videojs-hls-quality-selector on your website or web application, use any of the following methods. 63 | 64 | ### ` 70 | 71 | 76 | ``` 77 | 78 | ### Browserify/CommonJS 79 | 80 | When using with Browserify, install videojs-hls-quality-selector via npm and `require` the plugin as you would any other module. 81 | 82 | ```js 83 | var videojs = require('video.js'); 84 | 85 | // The actual plugin function is exported by this module, but it is also 86 | // attached to the `Player.prototype`; so, there is no need to assign it 87 | // to a variable. 88 | require('videojs-hls-quality-selector'); 89 | 90 | var player = videojs('my-video'); 91 | 92 | player.hlsQualitySelector({ 93 | displayCurrentQuality: true, 94 | }); 95 | ``` 96 | 97 | ### RequireJS/AMD 98 | 99 | When using with RequireJS (or another AMD library), get the script in whatever way you prefer and `require` the plugin as you normally would: 100 | 101 | ```js 102 | require(['video.js', 'videojs-hls-quality-selector'], function(videojs) { 103 | var player = videojs('my-video'); 104 | 105 | player.hlsQualitySelector(); 106 | }); 107 | ``` 108 | 109 | ## License 110 | 111 | MIT. Copyright (c) Chris Boustead (chris@forgemotion.com) 112 | 113 | 114 | [videojs]: http://videojs.com/ 115 | -------------------------------------------------------------------------------- /example-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisboustead/videojs-hls-quality-selector/672888deb0dc41be18fcbc2e1c269d6963363f9c/example-2.png -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisboustead/videojs-hls-quality-selector/672888deb0dc41be18fcbc2e1c269d6963363f9c/example.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | videojs-hls-quality-selector Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Auto": "Auto", 3 | "Quality": "Quality" 4 | } 5 | -------------------------------------------------------------------------------- /lang/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Auto": "Auto", 3 | "Quality": "Qualité" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videojs-hls-quality-selector", 3 | "version": "2.0.0", 4 | "description": "Adds a quality selector menu for HLS sources played in videojs.", 5 | "main": "cjs/plugin.js", 6 | "module": "es/plugin.js", 7 | "browser": "dist/videojs-hls-quality-selector.js", 8 | "generator-videojs-plugin": { 9 | "version": "9.0.0" 10 | }, 11 | "scripts": { 12 | "build": "npm-run-all -s clean -p build:*", 13 | "build-prod": "cross-env-shell NO_TEST_BUNDLE=1 'npm run build'", 14 | "build-test": "cross-env-shell TEST_BUNDLE_ONLY=1 'npm run build'", 15 | "build:cjs": "babel-config-cjs -d ./cjs ./src", 16 | "build:css": "postcss -o dist/videojs-hls-quality-selector.css --config scripts/postcss.config.js src/plugin.css", 17 | "build:es": "babel-config-es -d ./es ./src", 18 | "build:js": "rollup -c scripts/rollup.config.js", 19 | "build:lang": "vjslang --dir dist/lang", 20 | "clean": "shx rm -rf ./dist ./test/dist ./cjs ./es && shx mkdir -p ./dist ./test/dist ./cjs ./es", 21 | "docs": "npm-run-all docs:*", 22 | "docs:api": "jsdoc src -r -d docs/api", 23 | "docs:toc": "doctoc --notitle README.md", 24 | "lint": "vjsstandard", 25 | "server": "karma start scripts/karma.conf.js --singleRun=false --auto-watch", 26 | "start": "npm-run-all -p server watch", 27 | "test": "npm-run-all lint build-test && karma start scripts/karma.conf.js", 28 | "posttest": "shx cat test/dist/coverage/text.txt", 29 | "update-changelog": "conventional-changelog -p videojs -i CHANGELOG.md -s", 30 | "preversion": "npm test", 31 | "version": "is-prerelease || npm run update-changelog && git add CHANGELOG.md", 32 | "watch": "npm-run-all -p watch:*", 33 | "watch:cjs": "npm run build:cjs -- -w", 34 | "watch:css": "npm run build:css -- -w", 35 | "watch:es": "npm run build:es -- -w", 36 | "watch:js": "npm run build:js -- -w", 37 | "prepublishOnly": "npm-run-all build-prod && vjsverify --verbose --skip-es-check" 38 | }, 39 | "engines": { 40 | "node": ">=14", 41 | "npm": ">=6" 42 | }, 43 | "keywords": [ 44 | "videojs", 45 | "videojs-plugin" 46 | ], 47 | "author": "Chris Boustead (chris@forgemotion.com)", 48 | "license": "MIT", 49 | "vjsstandard": { 50 | "ignore": [ 51 | "es", 52 | "cjs", 53 | "dist", 54 | "docs", 55 | "test/dist" 56 | ] 57 | }, 58 | "files": [ 59 | "CONTRIBUTING.md", 60 | "cjs/", 61 | "dist/", 62 | "docs/", 63 | "es/", 64 | "index.html", 65 | "scripts/", 66 | "src/", 67 | "test/" 68 | ], 69 | "husky": { 70 | "hooks": { 71 | "pre-commit": "lint-staged" 72 | } 73 | }, 74 | "lint-staged": { 75 | "*.js": "vjsstandard --fix", 76 | "README.md": "doctoc --notitle" 77 | }, 78 | "dependencies": { 79 | "global": "^4.4.0", 80 | "video.js": "^8" 81 | }, 82 | "devDependencies": { 83 | "@babel/cli": "^7.14.3", 84 | "@babel/runtime": "^7.14.0", 85 | "@videojs/babel-config": "^0.2.0", 86 | "@videojs/generator-helpers": "~3.0.0", 87 | "jsdoc": "~3.6.7", 88 | "karma": "^6.3.2", 89 | "postcss": "^8.3.0", 90 | "postcss-cli": "^8.3.1", 91 | "rollup": "^2.50.3", 92 | "sinon": "^9.1.0", 93 | "videojs-generate-karma-config": "~8.0.0", 94 | "videojs-generate-postcss-config": "~3.0.0", 95 | "videojs-generate-rollup-config": "~7.0.1", 96 | "videojs-generator-verify": "~4.0.0", 97 | "videojs-languages": "^2.0.0", 98 | "videojs-standard": "^9.0.0" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /scripts/karma.conf.js: -------------------------------------------------------------------------------- 1 | const generate = require('videojs-generate-karma-config'); 2 | 3 | module.exports = function(config) { 4 | 5 | // see https://github.com/videojs/videojs-generate-karma-config 6 | // for options 7 | const options = {}; 8 | 9 | config = generate(config, options); 10 | 11 | // any other custom stuff not supported by options here! 12 | }; 13 | -------------------------------------------------------------------------------- /scripts/postcss.config.js: -------------------------------------------------------------------------------- 1 | const generate = require('videojs-generate-postcss-config'); 2 | 3 | module.exports = function(context) { 4 | const result = generate({}, context); 5 | 6 | // do custom stuff here 7 | 8 | return result; 9 | }; 10 | -------------------------------------------------------------------------------- /scripts/rollup.config.js: -------------------------------------------------------------------------------- 1 | const generate = require('videojs-generate-rollup-config'); 2 | 3 | // see https://github.com/videojs/videojs-generate-rollup-config 4 | // for options 5 | const options = {}; 6 | const config = generate(options); 7 | 8 | // Add additonal builds/customization here! 9 | 10 | // do not build module dists with rollup 11 | // this is handled by build:es and build:cjs 12 | if (config.builds.module) { 13 | delete config.builds.module; 14 | } 15 | 16 | // export the builds to rollup 17 | export default Object.values(config.builds); 18 | -------------------------------------------------------------------------------- /src/ConcreteButton.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | 3 | const MenuButton = videojs.getComponent('MenuButton'); 4 | const Menu = videojs.getComponent('Menu'); 5 | const Component = videojs.getComponent('Component'); 6 | const Dom = videojs.dom; 7 | 8 | /** 9 | * Convert string to title case. 10 | * 11 | * @param {string} string - the string to convert 12 | * @return {string} the returned titlecase string 13 | */ 14 | function toTitleCase(string) { 15 | if (typeof string !== 'string') { 16 | return string; 17 | } 18 | 19 | return string.charAt(0).toUpperCase() + string.slice(1); 20 | } 21 | 22 | /** 23 | * Extend vjs button class for quality button. 24 | */ 25 | export default class ConcreteButton extends MenuButton { 26 | 27 | /** 28 | * Button constructor. 29 | * 30 | * @param {Player} player - videojs player instance 31 | */ 32 | constructor(player) { 33 | super(player, { 34 | title: player.localize('Quality'), 35 | name: 'QualityButton' 36 | }); 37 | } 38 | 39 | /** 40 | * Creates button items. 41 | * 42 | * @return {Array} - Button items 43 | */ 44 | createItems() { 45 | return []; 46 | } 47 | 48 | /** 49 | * Create the menu and add all items to it. 50 | * 51 | * @return {Menu} 52 | * The constructed menu 53 | */ 54 | createMenu() { 55 | const menu = new Menu(this.player_, { menuButton: this }); 56 | 57 | this.hideThreshold_ = 0; 58 | 59 | // Add a title list item to the top 60 | if (this.options_.title) { 61 | const titleEl = Dom.createEl('li', { 62 | className: 'vjs-menu-title', 63 | innerHTML: toTitleCase(this.options_.title), 64 | tabIndex: -1 65 | }); 66 | const titleComponent = new Component(this.player_, { el: titleEl }); 67 | 68 | this.hideThreshold_ += 1; 69 | 70 | menu.addItem(titleComponent); 71 | } 72 | 73 | this.items = this.createItems(); 74 | 75 | if (this.items) { 76 | // Add menu items to the menu 77 | for (let i = 0; i < this.items.length; i++) { 78 | menu.addItem(this.items[i]); 79 | } 80 | } 81 | 82 | return menu; 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/ConcreteMenuItem.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | 3 | // Concrete classes 4 | const VideoJsMenuItemClass = videojs.getComponent('MenuItem'); 5 | 6 | /** 7 | * Extend vjs menu item class. 8 | */ 9 | export default class ConcreteMenuItem extends VideoJsMenuItemClass { 10 | 11 | /** 12 | * Menu item constructor. 13 | * 14 | * @param {Player} player - vjs player 15 | * @param {Object} item - Item object 16 | * @param {ConcreteButton} qualityButton - The containing button. 17 | * @param {HlsQualitySelector} plugin - This plugin instance. 18 | */ 19 | constructor(player, item, qualityButton, plugin) { 20 | super(player, { 21 | label: item.label, 22 | selectable: true, 23 | selected: item.selected || false 24 | }); 25 | this.item = item; 26 | this.qualityButton = qualityButton; 27 | this.plugin = plugin; 28 | } 29 | 30 | /** 31 | * Click event for menu item. 32 | */ 33 | handleClick() { 34 | 35 | // Reset other menu items selected status. 36 | for (let i = 0; i < this.qualityButton.items.length; ++i) { 37 | this.qualityButton.items[i].selected(false); 38 | } 39 | 40 | // Set this menu item to selected, and set quality. 41 | this.plugin.setQuality(this.item.value); 42 | this.selected(true); 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/plugin.css: -------------------------------------------------------------------------------- 1 | /** 2 | * css for videojs-hls-quality-selector 3 | * With the default plugins for postcss you can 4 | * - @import files, they will be inlined during build 5 | * - not worry about browser prefixes, they will be handled 6 | * - nest selectors. This follows the css specification that is 7 | * currently out on some browsers. See https://tabatkins.github.io/specs/css-nesting/ 8 | * - custom properties (aka variables) via the var(--var-name) syntax. See 9 | * https://www.w3.org/TR/css-variables-1/ 10 | */ 11 | 12 | 13 | /* Note: all vars must be defined here, there are no "local" vars */ 14 | /*:root { 15 | --main-color: red; 16 | --base-font-size: 9; 17 | --font-size: 7; 18 | }*/ 19 | 20 | .video-js { 21 | 22 | &.vjs-hls-quality-selector { 23 | /* This class is added to the video.js element by the plugin by default. */ 24 | display: block; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | import { version as VERSION } from '../package.json'; 3 | import ConcreteButton from './ConcreteButton'; 4 | import ConcreteMenuItem from './ConcreteMenuItem'; 5 | 6 | const Plugin = videojs.getPlugin('plugin'); 7 | 8 | // Default options for the plugin. 9 | const defaults = {}; 10 | 11 | /** 12 | * An advanced Video.js plugin. For more information on the API 13 | * 14 | * See: https://blog.videojs.com/feature-spotlight-advanced-plugins/ 15 | */ 16 | class HlsQualitySelector extends Plugin { 17 | 18 | /** 19 | * Create a HlsQualitySelector plugin instance. 20 | * 21 | * @param {Player} player 22 | * A Video.js Player instance. 23 | * 24 | * @param {Object} [options] 25 | * An optional options object. 26 | * 27 | * While not a core part of the Video.js plugin architecture, a 28 | * second argument of options is a convenient way to accept inputs 29 | * from your plugin's caller. 30 | */ 31 | constructor(player, options) { 32 | // the parent class will add player under this.player 33 | super(player); 34 | 35 | this.options = videojs.obj.merge(defaults, options); 36 | 37 | this.player.ready(() => { 38 | // If there is quality levels plugin and the HLS tech exists then continue. 39 | if (this.player.qualityLevels) { 40 | this.player.addClass('vjs-hls-quality-selector'); 41 | // Create the quality button. 42 | this.createQualityButton(); 43 | this.bindPlayerEvents(); 44 | } 45 | }); 46 | } 47 | 48 | /** 49 | * Binds listener for quality level changes. 50 | */ 51 | bindPlayerEvents() { 52 | this.player.qualityLevels().on('addqualitylevel', this.onAddQualityLevel.bind(this)); 53 | } 54 | 55 | /** 56 | * Adds the quality menu button to the player control bar. 57 | */ 58 | createQualityButton() { 59 | 60 | const player = this.player; 61 | 62 | this._qualityButton = new ConcreteButton(player); 63 | 64 | const placementIndex = player.controlBar.children().length - 2; 65 | const concreteButtonInstance = player.controlBar.addChild( 66 | this._qualityButton, 67 | { componentClass: 'qualitySelector' }, 68 | this.options.placementIndex || placementIndex 69 | ); 70 | 71 | concreteButtonInstance.addClass('vjs-quality-selector'); 72 | if (!this.options.displayCurrentQuality) { 73 | const icon = ` ${this.options.vjsIconClass || 'vjs-icon-hd'}`; 74 | 75 | concreteButtonInstance 76 | .menuButton_.$('.vjs-icon-placeholder').className += icon; 77 | } else { 78 | this.setButtonInnerText(player.localize('Auto')); 79 | } 80 | concreteButtonInstance.removeClass('vjs-hidden'); 81 | 82 | } 83 | 84 | /** 85 | *Set inner button text. 86 | * 87 | * @param {string} text - the text to display in the button. 88 | */ 89 | setButtonInnerText(text) { 90 | this._qualityButton 91 | .menuButton_.$('.vjs-icon-placeholder').innerHTML = text; 92 | } 93 | 94 | /** 95 | * Builds individual quality menu items. 96 | * 97 | * @param {Object} item - Individual quality menu item. 98 | * @return {ConcreteMenuItem} - Menu item 99 | */ 100 | getQualityMenuItem(item) { 101 | const player = this.player; 102 | 103 | return new ConcreteMenuItem(player, item, this._qualityButton, this); 104 | } 105 | 106 | /** 107 | * Executed when a quality level is added from HLS playlist. 108 | */ 109 | onAddQualityLevel() { 110 | const player = this.player; 111 | const qualityList = player.qualityLevels(); 112 | const levels = qualityList.levels_ || []; 113 | const levelItems = []; 114 | 115 | for (let i = 0; i < levels.length; ++i) { 116 | const { width, height } = levels[i]; 117 | const pixels = width > height ? height : width; 118 | 119 | if (!pixels) { 120 | continue; 121 | } 122 | 123 | if (!levelItems.filter(_existingItem => { 124 | return _existingItem.item && _existingItem.item.value === pixels; 125 | }).length) { 126 | const levelItem = this.getQualityMenuItem.call(this, { 127 | label: pixels + 'p', 128 | value: pixels 129 | }); 130 | 131 | levelItems.push(levelItem); 132 | } 133 | } 134 | 135 | levelItems.sort((current, next) => { 136 | if ((typeof current !== 'object') || (typeof next !== 'object')) { 137 | return -1; 138 | } 139 | if (current.item.value < next.item.value) { 140 | return -1; 141 | } 142 | if (current.item.value > next.item.value) { 143 | return 1; 144 | } 145 | return 0; 146 | }); 147 | 148 | levelItems.push(this.getQualityMenuItem.call(this, { 149 | label: this.player.localize('Auto'), 150 | value: 'auto', 151 | selected: true 152 | })); 153 | 154 | if (this._qualityButton) { 155 | this._qualityButton.createItems = () => { 156 | return levelItems; 157 | }; 158 | this._qualityButton.update(); 159 | } 160 | 161 | } 162 | 163 | /** 164 | * Sets quality (based on media short side) 165 | * 166 | * @param {number} quality - A number representing HLS playlist. 167 | */ 168 | setQuality(quality) { 169 | const qualityList = this.player.qualityLevels(); 170 | 171 | // Set quality on plugin 172 | this._currentQuality = quality; 173 | 174 | if (this.options.displayCurrentQuality) { 175 | this.setButtonInnerText(quality === 'auto' ? this.player.localize('Auto') : `${quality}p`); 176 | } 177 | 178 | for (let i = 0; i < qualityList.length; ++i) { 179 | const { width, height } = qualityList[i]; 180 | const pixels = width > height ? height : width; 181 | 182 | qualityList[i].enabled = (pixels === quality || quality === 'auto'); 183 | } 184 | this._qualityButton.unpressButton(); 185 | } 186 | 187 | /** 188 | * Return the current set quality or 'auto' 189 | * 190 | * @return {string} the currently set quality 191 | */ 192 | getCurrentQuality() { 193 | return this._currentQuality || 'auto'; 194 | } 195 | 196 | } 197 | 198 | // Include the version number. 199 | HlsQualitySelector.VERSION = VERSION; 200 | 201 | // Register the plugin with video.js. 202 | videojs.registerPlugin('hlsQualitySelector', HlsQualitySelector); 203 | 204 | export default HlsQualitySelector; 205 | -------------------------------------------------------------------------------- /test/plugin.test.js: -------------------------------------------------------------------------------- 1 | import document from 'global/document'; 2 | 3 | import QUnit from 'qunit'; 4 | import sinon from 'sinon'; 5 | import videojs from 'video.js'; 6 | 7 | import plugin from '../src/plugin'; 8 | 9 | const Player = videojs.getComponent('Player'); 10 | 11 | QUnit.test('the environment is sane', function(assert) { 12 | assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists'); 13 | assert.strictEqual(typeof sinon, 'object', 'sinon exists'); 14 | assert.strictEqual(typeof videojs, 'function', 'videojs exists'); 15 | assert.strictEqual(typeof plugin, 'function', 'plugin is a function'); 16 | }); 17 | 18 | QUnit.module('videojs-hls-quality-selector', { 19 | 20 | beforeEach() { 21 | 22 | // Mock the environment's timers because certain things - particularly 23 | // player readiness - are asynchronous in video.js 5. This MUST come 24 | // before any player is created; otherwise, timers could get created 25 | // with the actual timer methods! 26 | this.clock = sinon.useFakeTimers(); 27 | 28 | this.fixture = document.getElementById('qunit-fixture'); 29 | this.video = document.createElement('video'); 30 | this.fixture.appendChild(this.video); 31 | this.player = videojs(this.video); 32 | }, 33 | 34 | afterEach() { 35 | this.player.dispose(); 36 | this.clock.restore(); 37 | } 38 | }); 39 | 40 | QUnit.test('registers itself with video.js', function(assert) { 41 | assert.expect(2); 42 | 43 | assert.strictEqual( 44 | typeof Player.prototype.hlsQualitySelector, 45 | 'function', 46 | 'videojs-hls-quality-selector plugin was registered' 47 | ); 48 | 49 | this.player.hlsQualitySelector(); 50 | 51 | // Tick the clock forward enough to trigger the player to be "ready". 52 | this.clock.tick(1); 53 | 54 | assert.ok( 55 | this.player.hasClass('vjs-hls-quality-selector'), 56 | 'the plugin adds a class to the player' 57 | ); 58 | }); 59 | --------------------------------------------------------------------------------