├── .nvmrc ├── .remarkignore ├── src ├── css │ ├── vjs.scss │ ├── _variables.scss │ ├── vjs-cdn.scss │ ├── components │ │ ├── _control-spacer.scss │ │ ├── _subtitles.scss │ │ ├── _error.scss │ │ ├── _chapters.scss │ │ ├── _captions.scss │ │ ├── _descriptions.scss │ │ ├── _close-button.scss │ │ ├── _play-pause.scss │ │ ├── _playback-rate.scss │ │ ├── _fullscreen.scss │ │ ├── _time.scss │ │ ├── _modal-dialog.scss │ │ ├── _picture-in-picture.scss │ │ ├── _button.scss │ │ ├── _slider.scss │ │ ├── _poster.scss │ │ ├── _audio.scss │ │ ├── _skip-buttons.scss │ │ ├── _transient-button.scss │ │ ├── _control.scss │ │ ├── menu │ │ │ ├── _menu-inline.scss │ │ │ ├── _menu-popup.scss │ │ │ └── _menu.scss │ │ ├── _title-bar.scss │ │ ├── _subs-caps.scss │ │ ├── _text-track.scss │ │ ├── _live.scss │ │ ├── _adaptive.scss │ │ ├── _control-bar.scss │ │ └── _big-play.scss │ ├── _print.scss │ ├── _icons.scss │ ├── _private-variables.scss │ ├── video-js.scss │ └── _utilities.scss └── js │ ├── index.js │ ├── utils │ ├── log.js │ ├── dom-data.js │ ├── num.js │ ├── promise.js │ ├── guid.js │ ├── stylesheet.js │ ├── spatial-navigation-key-codes.js │ ├── buffer.js │ ├── str.js │ ├── deprecate.js │ ├── filter-source.js │ ├── url.js │ ├── hooks.js │ └── mimetypes.js │ ├── debug.js │ ├── control-bar │ ├── volume-control │ │ ├── volume-level.js │ │ ├── check-mute-support.js │ │ └── check-volume-support.js │ ├── spacer-controls │ │ ├── spacer.js │ │ └── custom-control-spacer.js │ ├── time-controls │ │ ├── time-divider.js │ │ └── current-time-display.js │ ├── text-track-controls │ │ ├── subs-caps-menu-item.js │ │ ├── chapters-track-menu-item.js │ │ ├── subtitles-button.js │ │ ├── caption-settings-menu-item.js │ │ └── captions-button.js │ ├── track-button.js │ ├── control-bar.js │ ├── live-display.js │ ├── progress-control │ │ └── mouse-time-display.js │ ├── playback-rate-menu │ │ └── playback-rate-menu-item.js │ └── audio-track-controls │ │ └── audio-track-button.js │ ├── consts │ └── errors.js │ ├── loading-spinner.js │ ├── fullscreen-api.js │ ├── tracks │ ├── track-enums.js │ ├── text-track-settings-controls.js │ ├── track-types.js │ └── text-track-list.js │ ├── error-display.js │ └── tech │ └── loader.js ├── lang ├── en-GB.json ├── sr.json ├── ba.json ├── hr.json ├── fi.json ├── da.json ├── bg.json └── pt-PT.json ├── docs ├── legacy-docs │ ├── images │ │ └── logo.png │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── api │ │ ├── assets │ │ │ ├── create-doc-files-test.html │ │ │ ├── api-doc-template.html │ │ │ └── api-doc-template-min.html │ │ ├── md.html │ │ ├── js │ │ │ └── highlight-syntax.js │ │ ├── index.html │ │ ├── css │ │ │ └── api-index.css │ │ ├── seekhandle.html │ │ ├── captions-track.html │ │ ├── chapters-track.html │ │ ├── subtitlestrack.html │ │ └── texttrack.html │ ├── examples │ │ ├── shared │ │ │ └── example-captions.vtt │ │ ├── elephantsdream │ │ │ └── chapters.en.vtt │ │ └── simple-embed │ │ │ └── index.html │ ├── guides │ │ └── tracks.html │ ├── js │ │ └── home.js │ └── index.html ├── examples │ ├── index.html │ ├── shared │ │ └── example-captions.vtt │ ├── elephantsdream │ │ ├── chapters.en.vtt │ │ └── index.html │ └── simple-embed │ │ └── index.html ├── index.md └── _redirects ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── COLLABORATOR_GUIDE.md ├── .github ├── first-timers.yml ├── workflows │ ├── lock.yml │ ├── pr-titles.yml │ └── ci.yml ├── actions │ └── pr-titles.js ├── move.yml ├── PULL_REQUEST_TEMPLATE.md ├── first-timers-issue-template.md ├── config.yml └── stale.yml ├── .npmignore ├── test ├── require │ ├── node.js │ ├── browserify.js │ └── webpack.js ├── sinon.js ├── unit │ ├── utils │ │ ├── num.test.js │ │ ├── promise.test.js │ │ ├── str.test.js │ │ └── custom-element.test.js │ ├── loading-spinner.test.js │ ├── button.test.js │ ├── setup.test.js │ ├── tracks │ │ ├── text-track-select.test.js │ │ ├── track-baseline.js │ │ └── track.test.js │ ├── event-target.test.js │ ├── close-button.test.js │ └── error-display.test.js └── karma.conf.js ├── .browserslistrc ├── .editorconfig ├── composer.json ├── postcss.config.js ├── tsconfig.json ├── .babelrc ├── .gitignore ├── LICENSE ├── sandbox ├── language.html.example ├── live.html.example ├── liveui.html.example ├── hls.html.example ├── plugin.html.example ├── index.html.example ├── svg-icons-enabled.html.example ├── vertical-volume.html.example ├── load-media.html.example ├── userAction-click.html.example ├── focus-visible.html.example ├── descriptions.html.example └── noUITitleAttributes.html.example ├── .jsdoc.js └── index.html /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /.remarkignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | -------------------------------------------------------------------------------- /src/css/vjs.scss: -------------------------------------------------------------------------------- 1 | @import "video-js"; 2 | -------------------------------------------------------------------------------- /lang/en-GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "Color": "Colour" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /src/css/_variables.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: 'font' !default; 2 | -------------------------------------------------------------------------------- /src/css/vjs-cdn.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: '//vjs.zencdn.net/font/1.5.1'; 2 | 3 | @import 'video-js'; 4 | -------------------------------------------------------------------------------- /src/css/components/_control-spacer.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-custom-control-spacer { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /docs/legacy-docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/videojs/video.js/HEAD/docs/legacy-docs/images/logo.png -------------------------------------------------------------------------------- /docs/legacy-docs/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/videojs/video.js/HEAD/docs/legacy-docs/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /src/css/_print.scss: -------------------------------------------------------------------------------- 1 | @media print { 2 | .video-js > *:not(.vjs-tech):not(.vjs-poster) { 3 | visibility:hidden; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Video.js® Contributor Guide 2 | 3 | Please refer to: 4 | -------------------------------------------------------------------------------- /src/css/components/_subtitles.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-subtitles-button .vjs-icon-placeholder { 2 | @extend .vjs-icon-subtitles; 3 | } 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Video.js® Code of Conduct 2 | 3 | Please refer to: 4 | -------------------------------------------------------------------------------- /docs/legacy-docs/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/videojs/video.js/HEAD/docs/legacy-docs/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docs/legacy-docs/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/videojs/video.js/HEAD/docs/legacy-docs/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docs/legacy-docs/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/videojs/video.js/HEAD/docs/legacy-docs/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docs/legacy-docs/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/videojs/video.js/HEAD/docs/legacy-docs/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/css/components/_error.scss: -------------------------------------------------------------------------------- 1 | .vjs-error .vjs-error-display .vjs-modal-dialog-content { 2 | font-size: 1.4em; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /COLLABORATOR_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Video.js® Collaborator Guide 2 | 3 | Please refer to: 4 | -------------------------------------------------------------------------------- /.github/first-timers.yml: -------------------------------------------------------------------------------- 1 | repository: 'video.js' 2 | labels: 3 | - first-timers-only 4 | - unclaimed 5 | template: .github/first-timers-issue-template.md 6 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | import videojs from './video'; 2 | import 'videojs-contrib-quality-levels'; 3 | import '@videojs/http-streaming'; 4 | export default videojs; 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Exclude everything but the contents of the dist directory. 2 | **/* 3 | !dist/** 4 | dist/video-js-*.zip 5 | !es5/** 6 | !src/css/** 7 | !core.js 8 | !core.es.js 9 | -------------------------------------------------------------------------------- /test/require/node.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | try { 3 | require('../../'); 4 | } catch (e) { 5 | console.error(e); 6 | process.exit(1); 7 | } 8 | 9 | process.exit(0); 10 | -------------------------------------------------------------------------------- /src/css/components/_chapters.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-chapters-button .vjs-icon-placeholder { 2 | @extend .vjs-icon-chapters; 3 | } 4 | 5 | .vjs-chapters-button .vjs-menu ul { 6 | width: 24em; 7 | } 8 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | last 3 major versions 4 | Firefox ESR 5 | Chrome >= 53 6 | not dead 7 | not ie 11 8 | not baidu 7 9 | not and_qq 11 10 | not and_uc 12 11 | not op_mini all 12 | -------------------------------------------------------------------------------- /src/css/components/_captions.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-captions-button .vjs-icon-placeholder { 2 | @extend .vjs-icon-captions; 3 | } 4 | 5 | .video-js.vjs-audio-only-mode .vjs-captions-button { 6 | display: none; 7 | } 8 | -------------------------------------------------------------------------------- /src/css/components/_descriptions.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-descriptions-button .vjs-icon-placeholder { 2 | @extend .vjs-icon-audio-description; 3 | } 4 | 5 | .video-js.vjs-audio-only-mode .vjs-descriptions-button { 6 | display: none; 7 | } 8 | -------------------------------------------------------------------------------- /src/js/utils/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file log.js 3 | * @module log 4 | */ 5 | import CreateLogger from './create-logger.js'; 6 | 7 | const log = CreateLogger('VIDEOJS'); 8 | const createLogger = log.createLogger; 9 | 10 | export default log; 11 | export { createLogger }; 12 | -------------------------------------------------------------------------------- /src/js/debug.js: -------------------------------------------------------------------------------- 1 | import videojs from './video'; 2 | import 'videojs-contrib-quality-levels'; 3 | import '@videojs/http-streaming'; 4 | import DomData from './utils/dom-data.js'; 5 | 6 | videojs.DomData = DomData; 7 | 8 | videojs.log.level('debug'); 9 | export default videojs; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org - unify code style 2 | # plugins for text editors: editorconfig.org/#download 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true -------------------------------------------------------------------------------- /src/css/components/_close-button.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-control.vjs-close-button { 2 | cursor: pointer; 3 | height: 3em; 4 | position: absolute; 5 | right: 0; 6 | top: 0.5em; 7 | z-index: 2; 8 | 9 | & .vjs-icon-placeholder { 10 | @extend .vjs-icon-cancel; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/require/browserify.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | /* eslint-env qunit */ 3 | var videojs = require('../../'); 4 | 5 | QUnit.module('Browserify Require'); 6 | QUnit.test('videojs should be requirable and bundled via browserify', function(assert) { 7 | assert.ok(videojs, 'videojs is required properly'); 8 | }); 9 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videojs/video.js", 3 | "description": "An HTML5 video player.", 4 | "type": "library", 5 | "keywords": [ 6 | "videojs", 7 | "html5", 8 | "video", 9 | "player" 10 | ], 11 | "homepage": "https://www.videojs.com/", 12 | "license": "Apache-2.0" 13 | } 14 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // see https://github.com/browserslist/browserslist for more info 2 | const browsersList = ['last 3 major versions', 'Firefox ESR', 'Chrome >= 53', 'not dead', 'not ie 11', 'not baidu 7', 'not and_qq 11', 'not and_uc 12', 'not op_mini all']; 3 | 4 | module.exports = { 5 | plugins: [ 6 | require('autoprefixer')(browsersList) 7 | ] 8 | }; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/js/**/*"], 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "allowSyntheticDefaultImports": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "outDir": "dist/types", 9 | "declarationMap": true, 10 | "skipLibCheck": true, 11 | "checkJs": false, 12 | "preserveWatchOutput": true 13 | } 14 | } -------------------------------------------------------------------------------- /src/css/components/_play-pause.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-play-control { 2 | cursor: pointer; 3 | } 4 | .video-js .vjs-play-control .vjs-icon-placeholder { 5 | @include flex(none); 6 | @extend .vjs-icon-play; 7 | } 8 | .video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder { 9 | @extend .vjs-icon-pause; 10 | } 11 | .video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder { 12 | @extend .vjs-icon-replay; 13 | } 14 | -------------------------------------------------------------------------------- /test/require/webpack.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | /* eslint-env qunit */ 3 | var videojs = require('../../'); 4 | var videojsCore = require('../../core'); 5 | 6 | QUnit.module('Webpack Require'); 7 | QUnit.test('videojs should be requirable and bundled via webpack', function(assert) { 8 | assert.ok(videojs, 'videojs is required properly'); 9 | assert.ok(videojsCore, 'videojs core is required properly'); 10 | }); 11 | -------------------------------------------------------------------------------- /src/js/utils/dom-data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file dom-data.js 3 | * @module dom-data 4 | */ 5 | 6 | /** 7 | * Element Data Store. 8 | * 9 | * Allows for binding data to an element without putting it directly on the 10 | * element. Ex. Event listeners are stored here. 11 | * (also from jsninja.com, slightly modified and updated for closure compiler) 12 | * 13 | * @type {Object} 14 | * @private 15 | */ 16 | export default new WeakMap(); 17 | -------------------------------------------------------------------------------- /test/sinon.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | const s = document.createElement('script'); 4 | 5 | // on IE11 and Safari 9, load the last supported sinon version 6 | if (/(?:MSIE|Trident\/7.0|Version\/9.*Safari)/.test(navigator.userAgent)) { 7 | s.src = 'https://unpkg.com/sinon@9.2.4/pkg/sinon-no-sourcemaps.js'; 8 | } else { 9 | s.src = '/test/base/node_modules/sinon/pkg/sinon.js'; 10 | } 11 | 12 | document.write(s.outerHTML); 13 | -------------------------------------------------------------------------------- /docs/legacy-docs/api/assets/create-doc-files-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 |

Check the console for results

12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock Threads' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 1 * * 1,4' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | 11 | concurrency: 12 | group: lock 13 | 14 | jobs: 15 | action: 16 | if: ${{ github.repository_owner == 'videojs' }} 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: dessant/lock-threads@v4 20 | with: 21 | issue-inactive-days: '60' 22 | process-only: 'issues' 23 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": [ 7 | "last 3 major versions", 8 | "Firefox ESR", 9 | "Chrome >= 53", 10 | "not dead", 11 | "not ie 11", 12 | "not baidu 7", 13 | "not and_qq 11", 14 | "not and_uc 12", 15 | "not op_mini all" 16 | ], 17 | "bugfixes": true, 18 | "loose": true 19 | } 20 | ] 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /docs/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Index of video.js examples 7 | 8 | 9 | 10 |

Index of video.js examples

11 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/css/components/_playback-rate.scss: -------------------------------------------------------------------------------- 1 | // TODO: I feel like this should be a generic menu. Research later. 2 | .vjs-playback-rate > .vjs-menu-button, 3 | .vjs-playback-rate .vjs-playback-rate-value { 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | .vjs-playback-rate .vjs-playback-rate-value { 12 | pointer-events: none; 13 | font-size: 1.5em; 14 | line-height: 2; 15 | text-align: center; 16 | } 17 | 18 | .vjs-playback-rate .vjs-menu { 19 | width: 4em; 20 | left: 0em; 21 | } 22 | -------------------------------------------------------------------------------- /src/css/components/_fullscreen.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-fullscreen-control { 2 | cursor: pointer; 3 | @include flex(none); 4 | 5 | & .vjs-icon-placeholder { 6 | @extend .vjs-icon-fullscreen-enter; 7 | } 8 | } 9 | 10 | .video-js.vjs-audio-only-mode .vjs-fullscreen-control, 11 | .vjs-pip-window .vjs-fullscreen-control { 12 | display: none; 13 | } 14 | 15 | // Switch to the exit icon when the player is in fullscreen 16 | .video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder { 17 | @extend .vjs-icon-fullscreen-exit; 18 | } 19 | -------------------------------------------------------------------------------- /src/js/utils/num.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file num.js 3 | * @module num 4 | */ 5 | 6 | /** 7 | * Keep a number between a min and a max value 8 | * 9 | * @param {number} number 10 | * The number to clamp 11 | * 12 | * @param {number} min 13 | * The minimum value 14 | * @param {number} max 15 | * The maximum value 16 | * 17 | * @return {number} 18 | * the clamped number 19 | */ 20 | export function clamp(number, min, max) { 21 | number = Number(number); 22 | 23 | return Math.min(max, Math.max(min, isNaN(number) ? min : number)); 24 | } 25 | -------------------------------------------------------------------------------- /test/unit/utils/num.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env qunit */ 2 | import * as Num from '../../../src/js/utils/num'; 3 | 4 | QUnit.module('utils/num', function() { 5 | 6 | QUnit.module('clamp'); 7 | 8 | QUnit.test('keep a number between min/max values', function(assert) { 9 | assert.expect(5); 10 | assert.strictEqual(Num.clamp(5, 1, 10), 5); 11 | assert.strictEqual(Num.clamp(5, 1, 5), 5); 12 | assert.strictEqual(Num.clamp(5, 1, 2), 2); 13 | assert.strictEqual(Num.clamp(-1, 1, 10), 1); 14 | assert.strictEqual(Num.clamp(NaN, 1, 10), 1); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/css/components/_time.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-time-control { 2 | @include flex(none); 3 | font-size: 1em; 4 | line-height: 3em; 5 | min-width: 2em; 6 | width: auto; 7 | padding-left: 1em; 8 | padding-right: 1em; 9 | } 10 | 11 | .vjs-live .vjs-time-control, 12 | .vjs-live .vjs-time-divider, 13 | .video-js .vjs-current-time, 14 | .video-js .vjs-duration { 15 | display: none; 16 | } 17 | 18 | .vjs-time-divider { 19 | display: none; 20 | line-height: 3em; 21 | } 22 | 23 | .vjs-normalise-time-controls:not(.vjs-live) .vjs-time-control { 24 | display: flex; 25 | } 26 | -------------------------------------------------------------------------------- /.github/actions/pr-titles.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core'); 2 | const github = require('@actions/github'); 3 | 4 | (async function run() { 5 | const title = github.context.payload.pull_request?.title; 6 | const titleRegex = /^(chore|ci|docs|feat|fix|refactor|revert|test)(\(.+\))?!?: (.+)/; 7 | 8 | if (!!title.match(titleRegex)) { 9 | core.info('Pull request title is OK'); 10 | } else { 11 | core.setFailed('Please use conventional commit style for the PR title so the merged change appears in the changelog. See https://www.conventionalcommits.org/.'); 12 | } 13 | })(); 14 | -------------------------------------------------------------------------------- /src/css/components/_modal-dialog.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-modal-dialog { 2 | @extend %fill-parent; 3 | @include linear-gradient(180deg, rgba(0, 0, 0, 0.8), rgba(255, 255, 255, 0)); 4 | 5 | // This allows scrolling of content if need be. 6 | overflow: auto; 7 | } 8 | 9 | // Reset box-sizing inside the modal dialog. 10 | .video-js .vjs-modal-dialog > * { 11 | box-sizing: border-box; 12 | } 13 | 14 | .vjs-modal-dialog .vjs-modal-dialog-content { 15 | @extend %fill-parent; 16 | 17 | font-size: 1.2em; // 12px 18 | line-height: 1.5; // 18px 19 | padding: 20px 24px; 20 | z-index: 1; 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/files/* 3 | build/temp/* 4 | docs/api/* 5 | docs/apistyles 6 | dev.html 7 | projects 8 | .zenflow-log 9 | test/*.map 10 | .bunyipconfig.js 11 | .s3config.json 12 | .eslintcache 13 | 14 | node_modules 15 | npm-debug.log* 16 | 17 | sandbox/* 18 | !sandbox/*.example 19 | 20 | *.swp 21 | *.swo 22 | 23 | *.orig 24 | 25 | *results.xml 26 | *.log 27 | 28 | test/dist/* 29 | .coveralls.yml 30 | .sass-cache 31 | 32 | dist/* 33 | es5/* 34 | 35 | .idea/ 36 | 37 | core.js 38 | core.es.js 39 | 40 | # Ignore Chinese clones for now. 41 | lang/zh-Han*.json 42 | 43 | # netlify deploy 44 | deploy/ 45 | -------------------------------------------------------------------------------- /src/css/components/_picture-in-picture.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-picture-in-picture-control { 2 | cursor: pointer; 3 | @include flex(none); 4 | 5 | & .vjs-icon-placeholder { 6 | @extend .vjs-icon-picture-in-picture-enter; 7 | } 8 | } 9 | 10 | .video-js.vjs-audio-only-mode .vjs-picture-in-picture-control, 11 | .vjs-pip-window .vjs-picture-in-picture-control { 12 | display: none; 13 | } 14 | 15 | // Switch to the exit icon when the player is in Picture-in-Picture 16 | .video-js.vjs-picture-in-picture .vjs-picture-in-picture-control .vjs-icon-placeholder { 17 | @extend .vjs-icon-picture-in-picture-exit; 18 | } 19 | -------------------------------------------------------------------------------- /.github/move.yml: -------------------------------------------------------------------------------- 1 | # Configuration for move-issues - https://github.com/dessant/move-issues 2 | 3 | # Delete the command comment when it contains no other content 4 | deleteCommand: true 5 | 6 | # Close the source issue after moving 7 | closeSourceIssue: true 8 | 9 | # Lock the source issue after moving 10 | lockSourceIssue: false 11 | 12 | # Mention issue and comment authors 13 | mentionAuthors: true 14 | 15 | # Preserve mentions in the issue content 16 | keepContentMentions: false 17 | 18 | # Set custom aliases for targets 19 | # aliases: 20 | # r: repo 21 | # or: owner/repo 22 | 23 | # Repository to extend settings from 24 | # _extends: repo 25 | -------------------------------------------------------------------------------- /.github/workflows/pr-titles.yml: -------------------------------------------------------------------------------- 1 | name: PR title check 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, edited, synchronize] 6 | branches: [main] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.number }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | pr-title-lint: 14 | name: Should follow conventional commit spec 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: '20' 21 | - run: npm i @actions/core @actions/github 22 | - run: node .github/actions/pr-titles.js 23 | -------------------------------------------------------------------------------- /test/unit/loading-spinner.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env qunit */ 2 | import LoadingSpinner from '../../src/js/loading-spinner.js'; 3 | import TestHelpers from './test-helpers.js'; 4 | import videojs from '../../src/js/video.js'; 5 | 6 | QUnit.module('Loading Spinner', {}); 7 | 8 | QUnit.test('should localize on languagechange', function(assert) { 9 | const player = TestHelpers.makePlayer({}); 10 | const spinner = new LoadingSpinner(player); 11 | 12 | videojs.addLanguage('test', {'{1} is loading.': '{1} LOADING'}); 13 | player.language('test'); 14 | 15 | assert.equal(spinner.$('.vjs-control-text').textContent, 'Video Player LOADING', 'loading spinner text is localized'); 16 | }); 17 | -------------------------------------------------------------------------------- /test/unit/utils/promise.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env qunit */ 2 | import window from 'global/window'; 3 | import * as promise from '../../../src/js/utils/promise'; 4 | 5 | QUnit.module('utils/promise'); 6 | 7 | QUnit.test('can correctly identify a native Promise (if supported)', function(assert) { 8 | assert.ok(promise.isPromise(new window.Promise((resolve) => resolve())), 'a native Promise was recognized'); 9 | }); 10 | 11 | QUnit.test('can identify a Promise-like object', function(assert) { 12 | assert.notOk(promise.isPromise({}), 'an object without a `then` method is not Promise-like'); 13 | assert.ok(promise.isPromise({then: () => {}}), 'an object with a `then` method is Promise-like'); 14 | }); 15 | -------------------------------------------------------------------------------- /src/css/components/_button.scss: -------------------------------------------------------------------------------- 1 | .video-js button { 2 | background: none; 3 | border: none; 4 | color: inherit; 5 | display: inline-block; 6 | 7 | font-size: inherit; // IE in general. WTF. 8 | line-height: inherit; 9 | text-transform: none; 10 | text-decoration: none; 11 | transition: none; 12 | 13 | // Chrome < 83 14 | -webkit-appearance: none; 15 | appearance: none; 16 | } 17 | 18 | // Replacement for focus in case spatial navigation is enabled 19 | .video-js.vjs-spatial-navigation-enabled .vjs-button:focus { 20 | outline: 0.0625em solid rgba($primary-foreground-color, 1); 21 | box-shadow: none; 22 | } 23 | 24 | .vjs-control .vjs-button { 25 | width: 100%; 26 | height: 100%; 27 | } 28 | -------------------------------------------------------------------------------- /src/css/components/_slider.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-slider { 2 | position: relative; 3 | cursor: pointer; 4 | padding: 0; 5 | margin: 0 0.45em 0 0.45em; 6 | 7 | @include user-select(none); 8 | 9 | @include background-color-with-alpha($secondary-background-color, $secondary-background-transparency); 10 | } 11 | 12 | .video-js .vjs-slider.disabled { 13 | cursor: default; 14 | } 15 | 16 | .video-js .vjs-slider:focus { 17 | text-shadow: 0em 0em 1em rgba($primary-foreground-color, 1); 18 | 19 | @include box-shadow(0 0 1em $primary-foreground-color); 20 | } 21 | 22 | // Replacement for focus in case spatial navigation is enabled 23 | .video-js.vjs-spatial-navigation-enabled .vjs-slider:focus { 24 | outline: 0.0625em solid rgba($primary-foreground-color, 1); 25 | } 26 | -------------------------------------------------------------------------------- /src/js/control-bar/volume-control/volume-level.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file volume-level.js 3 | */ 4 | import Component from '../../component.js'; 5 | 6 | /** 7 | * Shows volume level 8 | * 9 | * @extends Component 10 | */ 11 | class VolumeLevel extends Component { 12 | 13 | /** 14 | * Create the `Component`'s DOM element 15 | * 16 | * @return {Element} 17 | * The element that was created. 18 | */ 19 | createEl() { 20 | const el = super.createEl('div', { 21 | className: 'vjs-volume-level' 22 | }); 23 | 24 | this.setIcon('circle', el); 25 | 26 | el.appendChild(super.createEl('span', { 27 | className: 'vjs-control-text' 28 | })); 29 | 30 | return el; 31 | } 32 | 33 | } 34 | 35 | Component.registerComponent('VolumeLevel', VolumeLevel); 36 | export default VolumeLevel; 37 | -------------------------------------------------------------------------------- /test/unit/button.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env qunit */ 2 | import Button from '../../src/js/button.js'; 3 | import TestHelpers from './test-helpers.js'; 4 | 5 | QUnit.module('Button'); 6 | 7 | QUnit.test('should localize its text', function(assert) { 8 | assert.expect(3); 9 | 10 | const player = TestHelpers.makePlayer({ 11 | language: 'es', 12 | languages: { 13 | es: { 14 | Play: 'Juego' 15 | } 16 | } 17 | }); 18 | 19 | const testButton = new Button(player); 20 | 21 | testButton.controlText_ = 'Play'; 22 | const el = testButton.createEl(); 23 | 24 | assert.ok(el.nodeName.toLowerCase().match('button')); 25 | assert.ok(el.innerHTML.match(/vjs-control-text"?[^<>]*>Juego/)); 26 | assert.equal(el.getAttribute('title'), 'Juego'); 27 | 28 | testButton.dispose(); 29 | player.dispose(); 30 | }); 31 | -------------------------------------------------------------------------------- /src/css/components/_poster.scss: -------------------------------------------------------------------------------- 1 | .vjs-poster { 2 | display: inline-block; 3 | vertical-align: middle; 4 | cursor: pointer; 5 | margin: 0; 6 | padding: 0; 7 | position: absolute; 8 | top: 0; 9 | right: 0; 10 | bottom: 0; 11 | left: 0; 12 | height: 100%; 13 | } 14 | 15 | // Hide the poster after the video has started playing and when native controls are used 16 | .vjs-has-started .vjs-poster, 17 | .vjs-using-native-controls .vjs-poster { 18 | display: none; 19 | } 20 | 21 | // Don't hide the poster if we're playing audio or when audio-poster-mode is true 22 | .vjs-audio.vjs-has-started .vjs-poster, 23 | .vjs-has-started.vjs-audio-poster-mode .vjs-poster, 24 | .vjs-pip-container.vjs-has-started .vjs-poster { 25 | display: block; 26 | } 27 | 28 | .vjs-poster img { 29 | width: 100%; 30 | height: 100%; 31 | object-fit: contain; 32 | } 33 | -------------------------------------------------------------------------------- /src/js/utils/promise.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Returns whether an object is `Promise`-like (i.e. has a `then` method). 4 | * 5 | * @param {Object} value 6 | * An object that may or may not be `Promise`-like. 7 | * 8 | * @return {boolean} 9 | * Whether or not the object is `Promise`-like. 10 | */ 11 | export function isPromise(value) { 12 | return value !== undefined && value !== null && typeof value.then === 'function'; 13 | } 14 | 15 | /** 16 | * Silence a Promise-like object. 17 | * 18 | * This is useful for avoiding non-harmful, but potentially confusing "uncaught 19 | * play promise" rejection error messages. 20 | * 21 | * @param {Object} value 22 | * An object that may or may not be `Promise`-like. 23 | */ 24 | export function silencePromise(value) { 25 | if (isPromise(value)) { 26 | value.then(null, (e) => {}); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/examples/shared/example-captions.vtt: -------------------------------------------------------------------------------- 1 | WEBVTT 2 | 3 | 00:00.700 --> 00:04.110 4 | Captions describe all relevant audio for the hearing impaired. 5 | [ Heroic music playing for a seagull ] 6 | 7 | 00:04.500 --> 00:05.000 8 | [ Splash!!! ] 9 | 10 | 00:05.100 --> 00:06.000 11 | [ Sploosh!!! ] 12 | 13 | 00:08.000 --> 00:09.225 14 | [ Splash...splash...splash splash splash ] 15 | 16 | 00:10.525 --> 00:11.255 17 | [ Splash, Sploosh again ] 18 | 19 | 00:13.500 --> 00:14.984 20 | Dolphin: eeeEEEEEeeee! 21 | 22 | 00:14.984 --> 00:16.984 23 | Dolphin: Squawk! eeeEEE? 24 | 25 | 00:25.000 --> 00:28.284 26 | [ A whole ton of splashes ] 27 | 28 | 00:29.500 --> 00:31.000 29 | Mine. Mine. Mine. 30 | 31 | 00:34.300 --> 00:36.000 32 | Shark: Chomp 33 | 34 | 00:36.800 --> 00:37.900 35 | Shark: CHOMP!!! 36 | 37 | 00:37.861 --> 00:41.193 38 | EEEEEEOOOOOOOOOOWHALENOISE 39 | 40 | 00:42.593 --> 00:45.611 41 | [ BIG SPLASH ] -------------------------------------------------------------------------------- /src/css/components/_audio.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-audio-button .vjs-icon-placeholder { 2 | @extend .vjs-icon-audio; 3 | } 4 | 5 | .video-js .vjs-audio-button + .vjs-menu .vjs-descriptions-menu-item .vjs-menu-item-text .vjs-icon-placeholder, 6 | .video-js .vjs-audio-button + .vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder { 7 | vertical-align: middle; 8 | display: inline-block; 9 | margin-bottom: -0.1em; 10 | } 11 | 12 | // Mark a main-desc-menu-item (main + description) or description item with a trailing Audio Description icon 13 | .video-js .vjs-audio-button + .vjs-menu .vjs-descriptions-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before, 14 | .video-js .vjs-audio-button + .vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before { 15 | font-family: VideoJS; 16 | content: " \f12e"; 17 | font-size: 1.5em; 18 | line-height: inherit; 19 | } 20 | -------------------------------------------------------------------------------- /docs/legacy-docs/examples/shared/example-captions.vtt: -------------------------------------------------------------------------------- 1 | WEBVTT 2 | 3 | 00:00.700 --> 00:04.110 4 | Captions describe all relevant audio for the hearing impaired. 5 | [ Heroic music playing for a seagull ] 6 | 7 | 00:04.500 --> 00:05.000 8 | [ Splash!!! ] 9 | 10 | 00:05.100 --> 00:06.000 11 | [ Sploosh!!! ] 12 | 13 | 00:08.000 --> 00:09.225 14 | [ Splash...splash...splash splash splash ] 15 | 16 | 00:10.525 --> 00:11.255 17 | [ Splash, Sploosh again ] 18 | 19 | 00:13.500 --> 00:14.984 20 | Dolphin: eeeEEEEEeeee! 21 | 22 | 00:14.984 --> 00:16.984 23 | Dolphin: Squawk! eeeEEE? 24 | 25 | 00:25.000 --> 00:28.284 26 | [ A whole ton of splashes ] 27 | 28 | 00:29.500 --> 00:31.000 29 | Mine. Mine. Mine. 30 | 31 | 00:34.300 --> 00:36.000 32 | Shark: Chomp 33 | 34 | 00:36.800 --> 00:37.900 35 | Shark: CHOMP!!! 36 | 37 | 00:37.861 --> 00:41.193 38 | EEEEEEOOOOOOOOOOWHALENOISE 39 | 40 | 00:42.593 --> 00:45.611 41 | [ BIG SPLASH ] -------------------------------------------------------------------------------- /src/js/utils/guid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file guid.js 3 | * @module guid 4 | */ 5 | 6 | // Default value for GUIDs. This allows us to reset the GUID counter in tests. 7 | // 8 | // The initial GUID is 3 because some users have come to rely on the first 9 | // default player ID ending up as `vjs_video_3`. 10 | // 11 | // See: https://github.com/videojs/video.js/pull/6216 12 | const _initialGuid = 3; 13 | 14 | /** 15 | * Unique ID for an element or function 16 | * 17 | * @type {Number} 18 | */ 19 | let _guid = _initialGuid; 20 | 21 | /** 22 | * Get a unique auto-incrementing ID by number that has not been returned before. 23 | * 24 | * @return {number} 25 | * A new unique ID. 26 | */ 27 | export function newGUID() { 28 | return _guid++; 29 | } 30 | 31 | /** 32 | * Reset the unique auto-incrementing ID for testing only. 33 | */ 34 | export function resetGuidInTestsOnly() { 35 | _guid = _initialGuid; 36 | } 37 | -------------------------------------------------------------------------------- /src/css/components/_skip-buttons.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-skip-forward-5 { 2 | cursor: pointer; 3 | & .vjs-icon-placeholder { 4 | @extend .vjs-icon-forward-5; 5 | } 6 | } 7 | 8 | .video-js .vjs-skip-forward-10 { 9 | cursor: pointer; 10 | & .vjs-icon-placeholder { 11 | @extend .vjs-icon-forward-10; 12 | } 13 | } 14 | .video-js .vjs-skip-forward-30 { 15 | cursor: pointer; 16 | & .vjs-icon-placeholder { 17 | @extend .vjs-icon-forward-30; 18 | } 19 | } 20 | 21 | .video-js .vjs-skip-backward-5 { 22 | cursor: pointer; 23 | & .vjs-icon-placeholder { 24 | @extend .vjs-icon-replay-5; 25 | } 26 | } 27 | 28 | .video-js .vjs-skip-backward-10 { 29 | cursor: pointer; 30 | & .vjs-icon-placeholder { 31 | @extend .vjs-icon-replay-10; 32 | } 33 | } 34 | 35 | .video-js .vjs-skip-backward-30 { 36 | cursor: pointer; 37 | & .vjs-icon-placeholder { 38 | @extend .vjs-icon-replay-30; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2010-present Video.js contributors 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | ------------------------------------------------------------------ 16 | Trademark Notice: 17 | “Video.js” is a registered trademark of Brightcove Inc. 18 | The project is maintained independently by the open-source community, 19 | governed by the Video.js Technical Steering Committee. -------------------------------------------------------------------------------- /docs/legacy-docs/api/assets/api-doc-template.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/legacy-docs/api/assets/api-doc-template-min.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/css/_icons.scss: -------------------------------------------------------------------------------- 1 | // CSS styles for SVG icons used throughout video.js. 2 | // 3 | // The goal is to replace all icons from the font family pulled from videojs/font entirely. 4 | // This project currently uses fonts. We want to replace this with SVGs from 5 | // images/icons.svg. This will ensure consitency between versions, as well as simplified 6 | // and straight-forward customization. 7 | 8 | // Default styling for all SVG icons 9 | .vjs-svg-icon { 10 | display: inline-block; 11 | background-repeat: no-repeat; 12 | background-position: center; 13 | 14 | fill: currentColor; 15 | height: 1.8em; 16 | width: 1.8em; 17 | 18 | // Overwrite any font content 19 | &:before { 20 | content: none !important; 21 | } 22 | } 23 | 24 | // SVG shadow on hover and focus 25 | .vjs-svg-icon:hover, 26 | .vjs-control:focus .vjs-svg-icon { 27 | -webkit-filter: drop-shadow(0 0 0.25em #fff); 28 | filter: drop-shadow(0 0 0.25em #fff); 29 | } 30 | -------------------------------------------------------------------------------- /src/js/consts/errors.js: -------------------------------------------------------------------------------- 1 | export default { 2 | NetworkBadStatus: 'networkbadstatus', 3 | NetworkRequestFailed: 'networkrequestfailed', 4 | NetworkRequestAborted: 'networkrequestaborted', 5 | NetworkRequestTimeout: 'networkrequesttimeout', 6 | NetworkBodyParserFailed: 'networkbodyparserfailed', 7 | StreamingHlsPlaylistParserError: 'streaminghlsplaylistparsererror', 8 | StreamingDashManifestParserError: 'streamingdashmanifestparsererror', 9 | StreamingContentSteeringParserError: 'streamingcontentsteeringparsererror', 10 | StreamingVttParserError: 'streamingvttparsererror', 11 | StreamingFailedToSelectNextSegment: 'streamingfailedtoselectnextsegment', 12 | StreamingFailedToDecryptSegment: 'streamingfailedtodecryptsegment', 13 | StreamingFailedToTransmuxSegment: 'streamingfailedtotransmuxsegment', 14 | StreamingFailedToAppendSegment: 'streamingfailedtoappendsegment', 15 | StreamingCodecsChangeError: 'streamingcodecschangeerror' 16 | }; 17 | -------------------------------------------------------------------------------- /src/js/control-bar/volume-control/check-mute-support.js: -------------------------------------------------------------------------------- 1 | /** @import Component from '../../component' */ 2 | /** @import Player from '../../player' */ 3 | 4 | /** 5 | * Check if muting volume is supported and if it isn't hide the mute toggle 6 | * button. 7 | * 8 | * @param {Component} self 9 | * A reference to the mute toggle button 10 | * 11 | * @param {Player} player 12 | * A reference to the player 13 | * 14 | * @private 15 | */ 16 | const checkMuteSupport = function(self, player) { 17 | // hide mute toggle button if it's not supported by the current tech 18 | if (player.tech_ && !player.tech_.featuresMuteControl) { 19 | self.addClass('vjs-hidden'); 20 | } 21 | 22 | self.on(player, 'loadstart', function() { 23 | if (!player.tech_.featuresMuteControl) { 24 | self.addClass('vjs-hidden'); 25 | } else { 26 | self.removeClass('vjs-hidden'); 27 | } 28 | }); 29 | }; 30 | 31 | export default checkMuteSupport; 32 | -------------------------------------------------------------------------------- /docs/examples/elephantsdream/chapters.en.vtt: -------------------------------------------------------------------------------- 1 | WEBVTT 2 | 3 | NOTE Created by Owen Edwards 2015. http://creativecommons.org/licenses/by/2.5/ 4 | NOTE Based on 'finalbreakdown.rtf', part of the prepoduction notes, which are: 5 | NOTE (c) Copyright 2006, Blender Foundation / 6 | NOTE Netherlands Media Art Institute / 7 | NOTE www.elephantsdream.org 8 | 9 | 1 10 | 00:00:00.000 --> 00:00:27.500 11 | Prologue 12 | 13 | 2 14 | 00:00:27.500 --> 00:01:10.000 15 | Switchboard trap 16 | 17 | 3 18 | 00:01:10.000 --> 00:03:25.000 19 | Telephone/Lecture 20 | 21 | 4 22 | 00:03:25.000 --> 00:04:52.000 23 | Typewriter 24 | 25 | 5 26 | 00:04:52.000 --> 00:06:19.500 27 | Proog shows Emo stuff 28 | 29 | 6 30 | 00:06:19.500 --> 00:07:09.000 31 | Which way 32 | 33 | 7 34 | 00:07:09.000 --> 00:07:45.000 35 | Emo flips out 36 | 37 | 8 38 | 00:07:45.000 --> 00:09:25.000 39 | Emo creates 40 | 41 | 9 42 | 00:09:25.000 --> 00:10:53.000 43 | Closing credits 44 | 45 | -------------------------------------------------------------------------------- /src/css/_private-variables.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | 3 | // Text, icons, hover states 4 | $primary-foreground-color: #fff !default; 5 | 6 | // Control backgrounds (control bar, big play, menus) 7 | $primary-background-color: #2B333F !default; 8 | $primary-background-transparency: 0.7 !default; 9 | 10 | // Hover states, slider backgrounds 11 | $secondary-background-color: color.adjust($primary-background-color, $lightness: 33%, $space: hsl) !default; 12 | $secondary-background-transparency: 0.5 !default; 13 | 14 | // Avoiding helvetica: issue #376 15 | $text-font-family: Arial, Helvetica, sans-serif !default; 16 | 17 | // Using the '--' naming for component-specific styles 18 | $big-play-button--border-size: 0.06666em !default; 19 | $big-play-button--width: 3em !default; 20 | $big-play-button--line-height: 1.5em !default; 21 | $big-play-button--height: $big-play-button--line-height + ($big-play-button--border-size * 2) !default; 22 | $big-play-button--transparency: 0.8 !default; 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | Please describe the change as necessary. 3 | If it's a feature or enhancement please be as detailed as possible. 4 | If it's a bug fix, please link the issue that it fixes or describe the bug in as much detail. 5 | 6 | ## Specific Changes proposed 7 | Please list the specific changes involved in this pull request. 8 | 9 | ## Requirements Checklist 10 | - [ ] Feature implemented / Bug fixed 11 | - [ ] If necessary, more likely in a feature request than a bug fix 12 | - [ ] Change has been verified in an actual browser (Chrome, Firefox, IE) 13 | - [ ] Unit Tests updated or fixed 14 | - [ ] Docs/guides updated 15 | - [ ] Example created ([starter template on JSBin](https://codepen.io/gkatsev/pen/GwZegv?editors=1000#0)) 16 | - [ ] Has no DOM changes which impact accessiblilty or trigger warnings (e.g. Chrome issues tab) 17 | - [ ] Has no changes to JSDoc which cause `npm run docs:api` to error 18 | - [ ] Reviewed by Two Core Contributors 19 | -------------------------------------------------------------------------------- /docs/legacy-docs/examples/elephantsdream/chapters.en.vtt: -------------------------------------------------------------------------------- 1 | WEBVTT 2 | 3 | NOTE Created by Owen Edwards 2015. http://creativecommons.org/licenses/by/2.5/ 4 | NOTE Based on 'finalbreakdown.rtf', part of the prepoduction notes, which are: 5 | NOTE (c) Copyright 2006, Blender Foundation / 6 | NOTE Netherlands Media Art Institute / 7 | NOTE www.elephantsdream.org 8 | 9 | 1 10 | 00:00:00.000 --> 00:00:27.500 11 | Prologue 12 | 13 | 2 14 | 00:00:27.500 --> 00:01:10.000 15 | Switchboard trap 16 | 17 | 3 18 | 00:01:10.000 --> 00:03:25.000 19 | Telephone/Lecture 20 | 21 | 4 22 | 00:03:25.000 --> 00:04:52.000 23 | Typewriter 24 | 25 | 5 26 | 00:04:52.000 --> 00:06:19.500 27 | Proog shows Emo stuff 28 | 29 | 6 30 | 00:06:19.500 --> 00:07:09.000 31 | Which way 32 | 33 | 7 34 | 00:07:09.000 --> 00:07:45.000 35 | Emo flips out 36 | 37 | 8 38 | 00:07:45.000 --> 00:09:25.000 39 | Emo creates 40 | 41 | 9 42 | 00:09:25.000 --> 00:10:53.000 43 | Closing credits 44 | 45 | -------------------------------------------------------------------------------- /src/js/utils/stylesheet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file stylesheet.js 3 | * @module stylesheet 4 | */ 5 | import document from 'global/document'; 6 | 7 | /** 8 | * Create a DOM style element given a className for it. 9 | * 10 | * @param {string} className 11 | * The className to add to the created style element. 12 | * 13 | * @return {Element} 14 | * The element that was created. 15 | */ 16 | export const createStyleElement = function(className) { 17 | const style = document.createElement('style'); 18 | 19 | style.className = className; 20 | 21 | return style; 22 | }; 23 | 24 | /** 25 | * Add text to a DOM element. 26 | * 27 | * @param {Element} el 28 | * The Element to add text content to. 29 | * 30 | * @param {string} content 31 | * The text to add to the element. 32 | */ 33 | export const setTextContent = function(el, content) { 34 | if (el.styleSheet) { 35 | el.styleSheet.cssText = content; 36 | } else { 37 | el.textContent = content; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/js/control-bar/volume-control/check-volume-support.js: -------------------------------------------------------------------------------- 1 | /** @import Component from '../../component' */ 2 | /** @import Player from '../../player' */ 3 | 4 | /** 5 | * Check if volume control is supported and if it isn't hide the 6 | * `Component` that was passed using the `vjs-hidden` class. 7 | * 8 | * @param {Component} self 9 | * The component that should be hidden if volume is unsupported 10 | * 11 | * @param {Player} player 12 | * A reference to the player 13 | * 14 | * @private 15 | */ 16 | const checkVolumeSupport = function(self, player) { 17 | // hide volume controls when they're not supported by the current tech 18 | if (player.tech_ && !player.tech_.featuresVolumeControl) { 19 | self.addClass('vjs-hidden'); 20 | } 21 | 22 | self.on(player, 'loadstart', function() { 23 | if (!player.tech_.featuresVolumeControl) { 24 | self.addClass('vjs-hidden'); 25 | } else { 26 | self.removeClass('vjs-hidden'); 27 | } 28 | }); 29 | }; 30 | 31 | export default checkVolumeSupport; 32 | -------------------------------------------------------------------------------- /sandbox/language.html.example: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | VideoJS Languages Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 24 | 25 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/legacy-docs/api/md.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/legacy-docs/api/js/highlight-syntax.js: -------------------------------------------------------------------------------- 1 | BCLSHighlighter = (function(hljs) { 2 | var codeBlocks = document.querySelectorAll('pre>code'), 3 | i, 4 | iMax, 5 | txt, 6 | reLT = new RegExp('<', 'g'), 7 | reGT = new RegExp('>;', 'g'); 8 | 9 | /** 10 | * tests for all the ways a variable might be undefined or not have a value 11 | * @param {*} x the variable to test 12 | * @return {Boolean} true if variable is defined and has a value 13 | */ 14 | function isDefined(x) { 15 | if (x === '' || x === null || x === undefined || x === NaN) { 16 | return false; 17 | } 18 | return true; 19 | }; 20 | 21 | if (isDefined(codeBlocks)) { 22 | iMax = codeBlocks.length; 23 | for (i = 0; i < iMax; i++) { 24 | txt = codeBlocks[i].innerHTML.toString(); 25 | txt = txt.replace(reLT, '<'); 26 | txt = txt.replace(reGT, '>'); 27 | codeBlocks[i].innerHTML = txt; 28 | hljs.highlightBlock(codeBlocks[i]); 29 | } 30 | } 31 | })(hljs); -------------------------------------------------------------------------------- /src/js/control-bar/spacer-controls/spacer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file spacer.js 3 | */ 4 | import Component from '../../component.js'; 5 | 6 | /** 7 | * Just an empty spacer element that can be used as an append point for plugins, etc. 8 | * Also can be used to create space between elements when necessary. 9 | * 10 | * @extends Component 11 | */ 12 | class Spacer extends Component { 13 | 14 | /** 15 | * Builds the default DOM `className`. 16 | * 17 | * @return {string} 18 | * The DOM `className` for this object. 19 | */ 20 | buildCSSClass() { 21 | return `vjs-spacer ${super.buildCSSClass()}`; 22 | } 23 | 24 | /** 25 | * Create the `Component`'s DOM element 26 | * 27 | * @return {Element} 28 | * The element that was created. 29 | */ 30 | createEl(tag = 'div', props = {}, attributes = {}) { 31 | if (!props.className) { 32 | props.className = this.buildCSSClass(); 33 | } 34 | 35 | return super.createEl(tag, props, attributes); 36 | } 37 | } 38 | 39 | Component.registerComponent('Spacer', Spacer); 40 | 41 | export default Spacer; 42 | -------------------------------------------------------------------------------- /src/css/components/_transient-button.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-transient-button { 2 | position: absolute; 3 | height: 3em; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | background-color: rgba(50, 50, 50, 0.5); 8 | cursor: pointer; 9 | opacity: 1; 10 | transition: opacity 1s; 11 | } 12 | 13 | .video-js:not(.vjs-has-started) .vjs-transient-button { 14 | display: none; 15 | } 16 | 17 | .video-js.not-hover .vjs-transient-button:not(.force-display), 18 | .video-js.vjs-user-inactive .vjs-transient-button:not(.force-display) { 19 | opacity: 0; 20 | } 21 | 22 | .video-js .vjs-transient-button span { 23 | padding: 0 0.5em; 24 | } 25 | 26 | .video-js .vjs-transient-button.vjs-left { 27 | left: 1em; 28 | } 29 | 30 | .video-js .vjs-transient-button.vjs-right { 31 | right: 1em; 32 | } 33 | 34 | .video-js .vjs-transient-button.vjs-top { 35 | top: 1em; 36 | } 37 | 38 | .video-js .vjs-transient-button.vjs-near-top { 39 | top: 4em; 40 | } 41 | 42 | .video-js .vjs-transient-button.vjs-bottom { 43 | bottom: 4em; 44 | } 45 | 46 | .video-js .vjs-transient-button:hover { 47 | background-color: rgba(50, 50, 50, 0.9); 48 | } 49 | -------------------------------------------------------------------------------- /docs/examples/simple-embed/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Video.js | HTML5 Video Player 6 | 7 | 8 | 9 | 10 | 11 | 12 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/js/utils/spatial-navigation-key-codes.js: -------------------------------------------------------------------------------- 1 | // /** 2 | // * @file spatial-navigation-keycode.js 3 | // */ 4 | 5 | import * as browser from './browser.js'; 6 | 7 | // Determine the keycode for the 'back' key based on the platform 8 | const backKeyCode = browser.IS_TIZEN ? 10009 : browser.IS_WEBOS ? 461 : 8; 9 | 10 | const SpatialNavKeyCodes = { 11 | codes: { 12 | play: 415, 13 | pause: 19, 14 | ff: 417, 15 | rw: 412, 16 | back: backKeyCode 17 | }, 18 | names: { 19 | 415: 'play', 20 | 19: 'pause', 21 | 417: 'ff', 22 | 412: 'rw', 23 | [backKeyCode]: 'back' 24 | }, 25 | 26 | isEventKey(event, keyName) { 27 | keyName = keyName.toLowerCase(); 28 | 29 | if (this.names[event.keyCode] && this.names[event.keyCode] === keyName) { 30 | return true; 31 | } 32 | return false; 33 | }, 34 | 35 | getEventName(event) { 36 | if (this.names[event.keyCode]) { 37 | return this.names[event.keyCode]; 38 | } else if (this.codes[event.code]) { 39 | const code = this.codes[event.code]; 40 | 41 | return this.names[code]; 42 | } 43 | return null; 44 | } 45 | }; 46 | 47 | export default SpatialNavKeyCodes; 48 | -------------------------------------------------------------------------------- /src/css/components/_control.scss: -------------------------------------------------------------------------------- 1 | // vjs-control might be better named vjs-button now. 2 | // It's used on both real buttons (play button) 3 | // and div buttons (menu buttons) 4 | .video-js .vjs-control { 5 | position: relative; 6 | text-align: center; 7 | margin: 0; 8 | padding: 0; 9 | height: 100%; 10 | width: 4em; 11 | @include flex(none); 12 | } 13 | 14 | .video-js .vjs-control.vjs-visible-text { 15 | width: auto; 16 | padding-left: 1em; 17 | padding-right: 1em; 18 | } 19 | 20 | .vjs-button > .vjs-icon-placeholder:before { 21 | font-size: 1.8em; 22 | line-height: 1.67; 23 | 24 | @extend %icon-default; 25 | } 26 | 27 | .vjs-button > .vjs-icon-placeholder { 28 | display: block; 29 | } 30 | 31 | .vjs-button > .vjs-svg-icon { 32 | display: inline-block; 33 | } 34 | 35 | // Replacement for focus outline 36 | .video-js .vjs-control:focus:before, 37 | .video-js .vjs-control:hover:before, 38 | .video-js .vjs-control:focus { 39 | text-shadow: 0em 0em 1em rgba($primary-foreground-color, 1); 40 | } 41 | 42 | // Hide control text visually, but have it available for screenreaders 43 | .video-js *:not(.vjs-visible-text) > .vjs-control-text { 44 | @include hide-visually; 45 | } 46 | -------------------------------------------------------------------------------- /src/js/control-bar/spacer-controls/custom-control-spacer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file custom-control-spacer.js 3 | */ 4 | import Spacer from './spacer.js'; 5 | import Component from '../../component.js'; 6 | 7 | /** 8 | * Spacer specifically meant to be used as an insertion point for new plugins, etc. 9 | * 10 | * @extends Spacer 11 | */ 12 | class CustomControlSpacer extends Spacer { 13 | 14 | /** 15 | * Builds the default DOM `className`. 16 | * 17 | * @return {string} 18 | * The DOM `className` for this object. 19 | */ 20 | buildCSSClass() { 21 | return `vjs-custom-control-spacer ${super.buildCSSClass()}`; 22 | } 23 | 24 | /** 25 | * Create the `Component`'s DOM element 26 | * 27 | * @return {Element} 28 | * The element that was created. 29 | */ 30 | createEl() { 31 | return super.createEl('div', { 32 | className: this.buildCSSClass(), 33 | // No-flex/table-cell mode requires there be some content 34 | // in the cell to fill the remaining space of the table. 35 | textContent: '\u00a0' 36 | }); 37 | } 38 | } 39 | 40 | Component.registerComponent('CustomControlSpacer', CustomControlSpacer); 41 | export default CustomControlSpacer; 42 | -------------------------------------------------------------------------------- /src/js/control-bar/time-controls/time-divider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file time-divider.js 3 | */ 4 | import Component from '../../component.js'; 5 | 6 | /** 7 | * The separator between the current time and duration. 8 | * Can be hidden if it's not needed in the design. 9 | * 10 | * @extends Component 11 | */ 12 | class TimeDivider extends Component { 13 | 14 | /** 15 | * Create the component's DOM element 16 | * 17 | * @return {Element} 18 | * The element that was created. 19 | */ 20 | createEl() { 21 | const el = super.createEl('div', { 22 | className: 'vjs-time-control vjs-time-divider' 23 | }, { 24 | // this element and its contents can be hidden from assistive techs since 25 | // it is made extraneous by the announcement of the control text 26 | // for the current time and duration displays 27 | 'aria-hidden': true 28 | }); 29 | 30 | const div = super.createEl('div'); 31 | const span = super.createEl('span', { 32 | textContent: '/' 33 | }); 34 | 35 | div.appendChild(span); 36 | el.appendChild(div); 37 | 38 | return el; 39 | } 40 | 41 | } 42 | 43 | Component.registerComponent('TimeDivider', TimeDivider); 44 | export default TimeDivider; 45 | -------------------------------------------------------------------------------- /src/js/utils/buffer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file buffer.js 3 | * @module buffer 4 | */ 5 | import { createTimeRange } from './time.js'; 6 | 7 | /** @import { TimeRange } from './time' */ 8 | 9 | /** 10 | * Compute the percentage of the media that has been buffered. 11 | * 12 | * @param {TimeRange} buffered 13 | * The current `TimeRanges` object representing buffered time ranges 14 | * 15 | * @param {number} duration 16 | * Total duration of the media 17 | * 18 | * @return {number} 19 | * Percent buffered of the total duration in decimal form. 20 | */ 21 | export function bufferedPercent(buffered, duration) { 22 | let bufferedDuration = 0; 23 | let start; 24 | let end; 25 | 26 | if (!duration) { 27 | return 0; 28 | } 29 | 30 | if (!buffered || !buffered.length) { 31 | buffered = createTimeRange(0, 0); 32 | } 33 | 34 | for (let i = 0; i < buffered.length; i++) { 35 | start = buffered.start(i); 36 | end = buffered.end(i); 37 | 38 | // buffered end can be bigger than duration by a very small fraction 39 | if (end > duration) { 40 | end = duration; 41 | } 42 | 43 | bufferedDuration += end - start; 44 | } 45 | 46 | return bufferedDuration / duration; 47 | } 48 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # [Video.js][vjs-website] API Documentation 2 | 3 | ## Generated API docs 4 | 5 | These [API docs][api] at [docs.videojs.com][vjs-docs] are automatically generated from the jsdoc comments in the code of the `main` branch of the [Video.js repository][vjs-gh]. You'll find specific details about functions, properties, and events. 6 | 7 | The most useful API doc to start with is usually the [Player][api-player] class. 8 | 9 | ## Guides 10 | 11 | More in-depth instructional [guides][vjs-guides] are found on the main [Video.js website][vjs-website]. The guides explain general topics and use cases (e.g. setup). 12 | 13 | The full list of articles is on the [guides page][vjs-guides]. If you are looking for troubleshooting information, try the [FAQ][vjs-faq] and [troubleshooting][vjs-troubleshooting] pages. 14 | 15 | [vjs-website]: https://videojs.com 16 | 17 | [vjs-docs]: https://docs.videojs.com 18 | 19 | [vjs-gh]: https://github.com/videojs/video.js 20 | 21 | [vjs-guides]: https://videojs.com/guides/ 22 | 23 | [vjs-faq]: https://videojs.com/guides/faqs/ 24 | 25 | [vjs-troubleshooting]: https://videojs.com/guides/troubleshooting/ 26 | 27 | [api]: https://docs.videojs.com 28 | 29 | [api-player]: https://docs.videojs.com/Player.html 30 | -------------------------------------------------------------------------------- /docs/legacy-docs/api/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

This documentation is for an outdated version of Video.js. See documentation for the current release. 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/unit/utils/str.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env qunit */ 2 | import * as Str from '../../../src/js/utils/str.js'; 3 | 4 | QUnit.module('utils/str'); 5 | 6 | QUnit.test('toTitleCase should make a string start with an uppercase letter', function(assert) { 7 | const foo = Str.toTitleCase('bar'); 8 | 9 | assert.ok(foo === 'Bar'); 10 | }); 11 | 12 | QUnit.test('titleCaseEquals compares whether the TitleCase of two strings is equal', function(assert) { 13 | assert.ok(Str.titleCaseEquals('foo', 'foo'), 'foo equals foo'); 14 | assert.ok(Str.titleCaseEquals('foo', 'Foo'), 'foo equals Foo'); 15 | assert.ok(Str.titleCaseEquals('Foo', 'foo'), 'Foo equals foo'); 16 | assert.ok(Str.titleCaseEquals('Foo', 'Foo'), 'Foo equals Foo'); 17 | 18 | assert.ok(Str.titleCaseEquals('fooBar', 'fooBar'), 'fooBar equals fooBar'); 19 | assert.notOk(Str.titleCaseEquals('fooBAR', 'fooBar'), 'fooBAR does not equal fooBar'); 20 | assert.notOk(Str.titleCaseEquals('foobar', 'fooBar'), 'foobar does not equal fooBar'); 21 | assert.notOk(Str.titleCaseEquals('fooBar', 'FOOBAR'), 'fooBar does not equal fooBAR'); 22 | }); 23 | 24 | QUnit.test('toLowerCase should make a string start with a lowercase letter', function(assert) { 25 | const foo = Str.toLowerCase('BAR'); 26 | 27 | assert.ok(foo === 'bAR'); 28 | }); 29 | -------------------------------------------------------------------------------- /docs/legacy-docs/api/css/api-index.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #333; 3 | margin: 6em; 4 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | } 6 | 7 | a { 8 | color: #437ABE; 9 | text-decoration: none; 10 | } 11 | 12 | a:hover { 13 | text-decoration: underline; 14 | } 15 | 16 | h1, h2, h3 { 17 | margin-left: -1em; 18 | } 19 | 20 | h1 { 21 | border-bottom: 1px #CCC solid; 22 | font-size: 2.5em; 23 | } 24 | h2 { 25 | font-size: 2em; 26 | } 27 | 28 | h3 { 29 | font-size: 1.5em; 30 | } 31 | #top { 32 | border: none; 33 | } 34 | div.indexColumn { 35 | display: inline-block; 36 | width: 30%; 37 | vertical-align: top; 38 | } 39 | 40 | @media (max-width: 700px) { 41 | div.indexColumn { 42 | display: block; 43 | width: 100%; 44 | vertical-align: top; 45 | } 46 | 47 | 48 | .indexHeader { 49 | padding-left: 3em; 50 | } 51 | div, .section { 52 | border: none; 53 | } 54 | 55 | .legacydocsnote { 56 | background-color: #ffdddd; 57 | border: 2px solid #ff0000; 58 | text-align: center; 59 | margin: 1em auto; 60 | padding: 1em; 61 | width: 80%; 62 | } 63 | 64 | .legacydocsnote::before { 65 | content: "⚠️"; 66 | font-size: 200%; 67 | vertical-align: middle; 68 | } -------------------------------------------------------------------------------- /docs/legacy-docs/api/seekhandle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

This documentation is for an outdated version of Video.js. See documentation for the current release. 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/legacy-docs/api/captions-track.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

This documentation is for an outdated version of Video.js. See documentation for the current release. 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/legacy-docs/api/chapters-track.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

This documentation is for an outdated version of Video.js. See documentation for the current release. 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/legacy-docs/api/subtitlestrack.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

This documentation is for an outdated version of Video.js. See documentation for the current release. 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/css/components/menu/_menu-inline.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-menu-button-inline { 2 | @include transition(all 0.4s); 3 | overflow: hidden; 4 | } 5 | 6 | .video-js .vjs-menu-button-inline:before { 7 | // Icon pseudoelement has a different base font size (1.8em), so we need to 8 | // account for that in the width. 4em (standard button width) divided by 1.8 9 | // to get the same button width as normal. 10 | width: 2.222222222em; 11 | } 12 | 13 | // Hover state 14 | .video-js .vjs-menu-button-inline:hover, 15 | .video-js .vjs-menu-button-inline:focus, 16 | .video-js .vjs-menu-button-inline.vjs-slider-active { 17 | // This width is currently specific to the inline volume bar. 18 | width: 12em; 19 | } 20 | 21 | .vjs-menu-button-inline .vjs-menu { 22 | opacity: 0; 23 | height: 100%; 24 | width: auto; 25 | 26 | position: absolute; 27 | left: 4em; 28 | top: 0; 29 | 30 | padding: 0; 31 | margin: 0; 32 | 33 | @include transition(all 0.4s); 34 | } 35 | 36 | .vjs-menu-button-inline:hover .vjs-menu, 37 | .vjs-menu-button-inline:focus .vjs-menu, 38 | .vjs-menu-button-inline.vjs-slider-active .vjs-menu { 39 | display: block; 40 | opacity: 1; 41 | } 42 | 43 | .vjs-menu-button-inline .vjs-menu-content { 44 | width: auto; 45 | height: 100%; 46 | margin: 0; 47 | overflow: hidden; 48 | } 49 | -------------------------------------------------------------------------------- /test/unit/setup.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env qunit */ 2 | import TestHelpers from './test-helpers.js'; 3 | import sinon from 'sinon'; 4 | import window from 'global/window'; 5 | 6 | QUnit.module('Setup'); 7 | 8 | QUnit.test('should set options from data-setup even if autoSetup is not called before initialisation', function(assert) { 9 | const el = TestHelpers.makeTag(); 10 | 11 | el.setAttribute( 12 | 'data-setup', 13 | '{"controls": true, "autoplay": false, "preload": "auto", "playsinline": true}' 14 | ); 15 | 16 | const player = TestHelpers.makePlayer({}, el); 17 | 18 | assert.ok(player.options_.controls === true); 19 | assert.ok(player.options_.autoplay === false); 20 | assert.ok(player.options_.preload === 'auto'); 21 | assert.ok(player.options_.playsinline === true); 22 | player.dispose(); 23 | }); 24 | 25 | QUnit.test('should log an error if data-setup has invalid JSON', function(assert) { 26 | const logError = sinon.spy(window.console, 'error'); 27 | 28 | const el = TestHelpers.makeTag(); 29 | 30 | el.setAttribute( 31 | 'data-setup', 32 | "{'controls': true}" 33 | ); 34 | 35 | const player = TestHelpers.makePlayer({}, el); 36 | 37 | assert.ok(logError.calledWith('VIDEOJS:', 'ERROR:', 'data-setup')); 38 | player.dispose(); 39 | window.console.error.restore(); 40 | }); 41 | -------------------------------------------------------------------------------- /docs/legacy-docs/api/texttrack.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

This documentation is for an outdated version of Video.js. See documentation for the current release. 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/css/components/_title-bar.scss: -------------------------------------------------------------------------------- 1 | .vjs-title-bar { 2 | 3 | // At a base inherited font-size of 10px, the title bar overall height should 4 | // be 96px with the area of text occupying the first 48px and the rest being 5 | // padding. This leaves plenty of room for the gradient to fade to 6 | // transparent while maintaining an WCAG AA-compliant contrast ratio (tested 7 | // using the TPGi Color Contrast Analyzer application) even on top of a solid 8 | // white background. 9 | @include linear-gradient( 10 | 180deg, 11 | rgba(0, 0, 0, 0.9) 0%, 12 | rgba(0, 0, 0, 0.7) 60%, 13 | rgba(0, 0, 0, 0) 100% 14 | ); 15 | font-size: 1.2em; // 12px 16 | line-height: 1.5; // 18px 17 | @include transition(opacity 0.1s); 18 | padding: 0.666em 1.333em 4em; // 8px 16px 48px 19 | pointer-events: none; 20 | position: absolute; 21 | top: 0; 22 | width: 100%; 23 | } 24 | 25 | // Hide if an error occurs 26 | .vjs-error .vjs-title-bar { 27 | display: none; 28 | } 29 | 30 | .vjs-title-bar-title, 31 | .vjs-title-bar-description { 32 | margin: 0; 33 | overflow: hidden; 34 | text-overflow: ellipsis; 35 | white-space: nowrap; 36 | } 37 | 38 | .vjs-title-bar-title { 39 | font-weight: bold; 40 | margin-bottom: 0.333em; // 4px 41 | } 42 | 43 | .vjs-playing.vjs-user-inactive .vjs-title-bar { 44 | opacity: 0; 45 | @include transition(opacity 1s); 46 | } 47 | -------------------------------------------------------------------------------- /lang/sr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Pusti", 3 | "Pause": "Pauza", 4 | "Current Time": "Trenutno vreme", 5 | "Duration": "Vreme trajanja", 6 | "Remaining Time": "Preostalo vreme", 7 | "Stream Type": "Način strimovanja", 8 | "LIVE": "UŽIVO", 9 | "Loaded": "Učitan", 10 | "Progress": "Progres", 11 | "Fullscreen": "Pun ekran", 12 | "Exit Fullscreen": "Mali ekran", 13 | "Mute": "Prigušen", 14 | "Unmute": "Ne-prigušen", 15 | "Playback Rate": "Stopa reprodukcije", 16 | "Subtitles": "Podnaslov", 17 | "subtitles off": "Podnaslov deaktiviran", 18 | "Captions": "Titlovi", 19 | "captions off": "Titlovi deaktivirani", 20 | "Chapters": "Poglavlja", 21 | "You aborted the media playback": "Isključili ste reprodukciju videa.", 22 | "A network error caused the media download to fail part-way.": "Video se prestao preuzimati zbog greške na mreži.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video se ne može reproducirati zbog servera, greške u mreži ili format nije podržan.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Reprodukcija videa je zaustavljenja zbog greške u formatu ili zbog verzije vašeg pretraživača.", 25 | "No compatible source was found for this media.": "Nije nađen nijedan kompatibilan izvor ovog videa." 26 | } 27 | -------------------------------------------------------------------------------- /src/js/loading-spinner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file loading-spinner.js 3 | */ 4 | import Component from './component'; 5 | import * as Dom from './utils/dom'; 6 | 7 | /** 8 | * A loading spinner for use during waiting/loading events. 9 | * 10 | * @extends Component 11 | */ 12 | class LoadingSpinner extends Component { 13 | 14 | /** 15 | * Create the `LoadingSpinner`s DOM element. 16 | * 17 | * @return {Element} 18 | * The dom element that gets created. 19 | */ 20 | createEl() { 21 | const isAudio = this.player_.isAudio(); 22 | const playerType = this.localize(isAudio ? 'Audio Player' : 'Video Player'); 23 | const controlText = Dom.createEl('span', { 24 | className: 'vjs-control-text', 25 | textContent: this.localize('{1} is loading.', [playerType]) 26 | }); 27 | 28 | const el = super.createEl('div', { 29 | className: 'vjs-loading-spinner', 30 | dir: 'ltr' 31 | }); 32 | 33 | el.appendChild(controlText); 34 | 35 | return el; 36 | } 37 | 38 | /** 39 | * Update control text on languagechange 40 | */ 41 | handleLanguagechange() { 42 | this.$('.vjs-control-text').textContent = this.localize('{1} is loading.', [ 43 | this.player_.isAudio() ? 'Audio Player' : 'Video Player' 44 | ]); 45 | } 46 | } 47 | 48 | Component.registerComponent('LoadingSpinner', LoadingSpinner); 49 | export default LoadingSpinner; 50 | -------------------------------------------------------------------------------- /lang/ba.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Pusti", 3 | "Pause": "Pauza", 4 | "Current Time": "Trenutno vrijeme", 5 | "Duration": "Vrijeme trajanja", 6 | "Remaining Time": "Preostalo vrijeme", 7 | "Stream Type": "Način strimovanja", 8 | "LIVE": "UŽIVO", 9 | "Loaded": "Učitan", 10 | "Progress": "Progres", 11 | "Fullscreen": "Puni ekran", 12 | "Exit Fullscreen": "Mali ekran", 13 | "Mute": "Prigušen", 14 | "Unmute": "Ne-prigušen", 15 | "Playback Rate": "Stopa reprodukcije", 16 | "Subtitles": "Podnaslov", 17 | "subtitles off": "Podnaslov deaktiviran", 18 | "Captions": "Titlovi", 19 | "captions off": "Titlovi deaktivirani", 20 | "Chapters": "Poglavlja", 21 | "You aborted the media playback": "Isključili ste reprodukciju videa.", 22 | "A network error caused the media download to fail part-way.": "Video se prestao preuzimati zbog greške na mreži.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video se ne može reproducirati zbog servera, greške u mreži ili je format ne podržan.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Reprodukcija videa je zaustavljenja zbog greške u formatu ili zbog verzije vašeg pretraživača.", 25 | "No compatible source was found for this media.": "Nije nađen nijedan kompatibilan izvor ovog videa." 26 | } 27 | -------------------------------------------------------------------------------- /lang/hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Pusti", 3 | "Pause": "Pauza", 4 | "Current Time": "Trenutno vrijeme", 5 | "Duration": "Vrijeme trajanja", 6 | "Remaining Time": "Preostalo vrijeme", 7 | "Stream Type": "Način strimovanja", 8 | "LIVE": "UŽIVO", 9 | "Loaded": "Učitan", 10 | "Progress": "Progres", 11 | "Fullscreen": "Puni ekran", 12 | "Exit Fullscreen": "Mali ekran", 13 | "Mute": "Prigušen", 14 | "Unmute": "Ne-prigušen", 15 | "Playback Rate": "Stopa reprodukcije", 16 | "Subtitles": "Podnaslov", 17 | "subtitles off": "Podnaslov deaktiviran", 18 | "Captions": "Titlovi", 19 | "captions off": "Titlovi deaktivirani", 20 | "Chapters": "Poglavlja", 21 | "You aborted the media playback": "Isključili ste reprodukciju videa.", 22 | "A network error caused the media download to fail part-way.": "Video se prestao preuzimati zbog greške na mreži.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video se ne može reproducirati zbog servera, greške u mreži ili je format ne podržan.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Reprodukcija videa je zaustavljenja zbog greške u formatu ili zbog verzije vašeg pretraživača.", 25 | "No compatible source was found for this media.": "Nije nađen nijedan kompatibilan izvor ovog videa." 26 | } 27 | -------------------------------------------------------------------------------- /src/js/utils/str.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file str.js 3 | * @module to-lower-case 4 | */ 5 | 6 | /** 7 | * Lowercase the first letter of a string. 8 | * 9 | * @param {string} string 10 | * String to be lowercased 11 | * 12 | * @return {string} 13 | * The string with a lowercased first letter 14 | */ 15 | export const toLowerCase = function(string) { 16 | if (typeof string !== 'string') { 17 | return string; 18 | } 19 | 20 | return string.replace(/./, (w) => w.toLowerCase()); 21 | }; 22 | 23 | /** 24 | * Uppercase the first letter of a string. 25 | * 26 | * @param {string} string 27 | * String to be uppercased 28 | * 29 | * @return {string} 30 | * The string with an uppercased first letter 31 | */ 32 | export const toTitleCase = function(string) { 33 | if (typeof string !== 'string') { 34 | return string; 35 | } 36 | 37 | return string.replace(/./, (w) => w.toUpperCase()); 38 | }; 39 | 40 | /** 41 | * Compares the TitleCase versions of the two strings for equality. 42 | * 43 | * @param {string} str1 44 | * The first string to compare 45 | * 46 | * @param {string} str2 47 | * The second string to compare 48 | * 49 | * @return {boolean} 50 | * Whether the TitleCase versions of the strings are equal 51 | */ 52 | export const titleCaseEquals = function(str1, str2) { 53 | return toTitleCase(str1) === toTitleCase(str2); 54 | }; 55 | -------------------------------------------------------------------------------- /test/unit/utils/custom-element.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import videojs from '../../../src/js/video.js'; 3 | 4 | export class TestCustomElement extends HTMLElement { 5 | 6 | constructor() { 7 | super(); 8 | 9 | const shadowRoot = this.attachShadow({ mode: 'closed' }); 10 | 11 | const containerElem = document.createElement('div'); 12 | 13 | containerElem.setAttribute('data-vjs-player', ''); 14 | shadowRoot.appendChild(containerElem); 15 | 16 | const videoElem = document.createElement('video'); 17 | 18 | videoElem.setAttribute('width', 640); 19 | videoElem.setAttribute('height', 260); 20 | containerElem.appendChild(videoElem); 21 | 22 | this.innerPlayer = videojs(videoElem); 23 | } 24 | } 25 | 26 | export class TestSlotElement extends HTMLElement { 27 | 28 | constructor() { 29 | super(); 30 | const shadowRoot = this.attachShadow({ mode: 'open' }); 31 | const wrapperEl = document.createElement('div'); 32 | 33 | wrapperEl.style = this.dataset.style; 34 | const slot = document.createElement('slot'); 35 | 36 | wrapperEl.appendChild(slot); 37 | shadowRoot.appendChild(wrapperEl); 38 | } 39 | } 40 | 41 | // Not supported on Chrome < 54 42 | if ('customElements' in window) { 43 | window.customElements.define('test-custom-element', TestCustomElement); 44 | window.customElements.define('test-slot-element', TestSlotElement); 45 | } 46 | -------------------------------------------------------------------------------- /lang/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Toisto", 3 | "Pause": "Tauko", 4 | "Current Time": "Tämänhetkinen aika", 5 | "Duration": "Kokonaisaika", 6 | "Remaining Time": "Jäljellä oleva aika", 7 | "Stream Type": "Lähetystyyppi", 8 | "LIVE": "LIVE", 9 | "Loaded": "Ladattu", 10 | "Progress": "Edistyminen", 11 | "Fullscreen": "Koko näyttö", 12 | "Exit Fullscreen": "Koko näyttö pois", 13 | "Mute": "Ääni pois", 14 | "Unmute": "Ääni päällä", 15 | "Playback Rate": "Toistonopeus", 16 | "Subtitles": "Tekstitys", 17 | "subtitles off": "Tekstitys pois", 18 | "Captions": "Tekstitys", 19 | "captions off": "Tekstitys pois", 20 | "Chapters": "Kappaleet", 21 | "You aborted the media playback": "Olet keskeyttänyt videotoiston.", 22 | "A network error caused the media download to fail part-way.": "Verkkovirhe keskeytti videon latauksen.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Videon lataus ei onnistunut joko palvelin- tai verkkovirheestä tai väärästä formaatista johtuen.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Videon toisto keskeytyi, koska media on vaurioitunut tai käyttää käyttää toimintoja, joita selaimesi ei tue.", 25 | "No compatible source was found for this media.": "Tälle videolle ei löytynyt yhteensopivaa lähdettä." 26 | } 27 | -------------------------------------------------------------------------------- /src/js/control-bar/text-track-controls/subs-caps-menu-item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file subs-caps-menu-item.js 3 | */ 4 | import TextTrackMenuItem from './text-track-menu-item.js'; 5 | import Component from '../../component.js'; 6 | import {createEl} from '../../utils/dom.js'; 7 | 8 | /** 9 | * SubsCapsMenuItem has an [cc] icon to distinguish captions from subtitles 10 | * in the SubsCapsMenu. 11 | * 12 | * @extends TextTrackMenuItem 13 | */ 14 | class SubsCapsMenuItem extends TextTrackMenuItem { 15 | 16 | createEl(type, props, attrs) { 17 | const el = super.createEl(type, props, attrs); 18 | const parentSpan = el.querySelector('.vjs-menu-item-text'); 19 | 20 | if (this.options_.track.kind === 'captions') { 21 | if (this.player_.options_.experimentalSvgIcons) { 22 | this.setIcon('captions', el); 23 | } else { 24 | parentSpan.appendChild(createEl('span', { 25 | className: 'vjs-icon-placeholder' 26 | }, { 27 | 'aria-hidden': true 28 | })); 29 | } 30 | parentSpan.appendChild(createEl('span', { 31 | className: 'vjs-control-text', 32 | // space added as the text will visually flow with the 33 | // label 34 | textContent: ` ${this.localize('Captions')}` 35 | })); 36 | } 37 | 38 | return el; 39 | } 40 | } 41 | 42 | Component.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem); 43 | export default SubsCapsMenuItem; 44 | -------------------------------------------------------------------------------- /src/css/components/_subs-caps.scss: -------------------------------------------------------------------------------- 1 | // North America uses 'CC' icon 2 | .video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder, 3 | .video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder { 4 | @extend .vjs-icon-captions; 5 | } 6 | 7 | // ROW uses 'subtitles' 8 | // Double selector because @extend puts these rules above the captions icon 9 | .video-js .vjs-subs-caps-button .vjs-icon-placeholder, 10 | .video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder, 11 | .video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder, 12 | .video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder, 13 | .video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder { 14 | @extend .vjs-icon-subtitles; 15 | } 16 | 17 | .vjs-subs-caps-button + .vjs-menu .vjs-captions-menu-item .vjs-svg-icon { 18 | width: 1.5em; 19 | height: 1.5em; 20 | } 21 | 22 | .video-js .vjs-subs-caps-button + .vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder { 23 | vertical-align: middle; 24 | display: inline-block; 25 | margin-bottom: -0.1em; 26 | } 27 | .video-js .vjs-subs-caps-button + .vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before { 28 | font-family: VideoJS; 29 | content: "\f10c"; 30 | font-size: 1.5em; 31 | line-height: inherit; 32 | } 33 | 34 | .video-js.vjs-audio-only-mode .vjs-subs-caps-button { 35 | display: none; 36 | } 37 | -------------------------------------------------------------------------------- /sandbox/live.html.example: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Video.js Sandbox 6 | 7 | 8 | 9 | 10 |

11 |

You can use /sandbox/ for writing and testing your own code. Nothing in /sandbox/ will get checked into the repo, except files that end in .example (so don't edit or add those files). To get started run `npm start` and open the live.html

12 |
npm start
13 |
open http://localhost:9999/sandbox/live.html
14 |
15 | 16 | 17 | 18 |

To view this video please enable JavaScript, and consider upgrading to a web browser that supports HTML5 video

19 |
20 | 21 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /lang/da.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Afspil", 3 | "Pause": "Pause", 4 | "Current Time": "Aktuel tid", 5 | "Duration": "Varighed", 6 | "Remaining Time": "Resterende tid", 7 | "Stream Type": "Stream-type", 8 | "LIVE": "LIVE", 9 | "Loaded": "Indlæst", 10 | "Progress": "Status", 11 | "Fullscreen": "Fuldskærm", 12 | "Exit Fullscreen": "Luk fuldskærm", 13 | "Mute": "Uden lyd", 14 | "Unmute": "Med lyd", 15 | "Playback Rate": "Afspilningsrate", 16 | "Subtitles": "Undertekster", 17 | "subtitles off": "Uden undertekster", 18 | "Captions": "Undertekster for hørehæmmede", 19 | "captions off": "Uden undertekster for hørehæmmede", 20 | "Chapters": "Kapitler", 21 | "You aborted the media playback": "Du afbrød videoafspilningen.", 22 | "A network error caused the media download to fail part-way.": "En netværksfejl fik download af videoen til at fejle.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Videoen kunne ikke indlæses, enten fordi serveren eller netværket fejlede, eller fordi formatet ikke er understøttet.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Videoafspilningen blev afbrudt på grund af ødelagte data eller fordi videoen benyttede faciliteter som din browser ikke understøtter.", 25 | "No compatible source was found for this media.": "Fandt ikke en kompatibel kilde for denne media." 26 | } 27 | -------------------------------------------------------------------------------- /lang/bg.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Възпроизвеждане", 3 | "Pause": "Пауза", 4 | "Current Time": "Текущо време", 5 | "Duration": "Продължителност", 6 | "Remaining Time": "Оставащо време", 7 | "Stream Type": "Тип на потока", 8 | "LIVE": "НА ЖИВО", 9 | "Loaded": "Заредено", 10 | "Progress": "Прогрес", 11 | "Fullscreen": "Цял екран", 12 | "Exit Fullscreen": "Спиране на цял екран", 13 | "Mute": "Без звук", 14 | "Unmute": "Със звук", 15 | "Playback Rate": "Скорост на възпроизвеждане", 16 | "Subtitles": "Субтитри", 17 | "subtitles off": "Спряни субтитри", 18 | "Captions": "Аудио надписи", 19 | "captions off": "Спряни аудио надписи", 20 | "Chapters": "Глави", 21 | "You aborted the media playback": "Спряхте възпроизвеждането на видеото", 22 | "A network error caused the media download to fail part-way.": "Грешка в мрежата провали изтеглянето на видеото.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Видеото не може да бъде заредено заради проблем със сървъра или мрежата или защото този формат не е поддържан.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Възпроизвеждането на видеото беше прекъснато заради проблем с файла или защото видеото използва опции които браузърът Ви не поддържа.", 25 | "No compatible source was found for this media.": "Не беше намерен съвместим източник за това видео." 26 | } 27 | -------------------------------------------------------------------------------- /test/unit/tracks/text-track-select.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env qunit */ 2 | import TestHelpers from '../test-helpers.js'; 3 | 4 | const tracks = [{ 5 | kind: 'captions', 6 | label: 'test' 7 | }]; 8 | 9 | QUnit.module('Text Track Select'); 10 | 11 | QUnit.test('should associate with