├── src ├── css │ ├── vjs.scss │ ├── _variables.scss │ ├── vjs-cdn.scss │ ├── components │ │ ├── _captions.scss │ │ ├── _control-spacer.scss │ │ ├── _subtitles.scss │ │ ├── _descriptions.scss │ │ ├── _chapters.scss │ │ ├── _close-button.scss │ │ ├── _play-pause.scss │ │ ├── _fullscreen.scss │ │ ├── _modal-dialog.scss │ │ ├── _playback-rate.scss │ │ ├── _live.scss │ │ ├── _button.scss │ │ ├── _slider.scss │ │ ├── _error.scss │ │ ├── menu │ │ │ ├── _menu-popup.scss │ │ │ ├── _menu.scss │ │ │ └── _menu-inline.scss │ │ ├── _time.scss │ │ ├── _control.scss │ │ ├── _text-track.scss │ │ ├── _poster.scss │ │ ├── _adaptive.scss │ │ ├── _control-bar.scss │ │ ├── _captions-settings.scss │ │ └── _big-play.scss │ ├── _private-variables.scss │ └── video-js.scss └── js │ ├── utils │ ├── guid.js │ ├── to-title-case.js │ ├── stylesheet.js │ ├── buffer.js │ ├── fn.js │ ├── format-time.js │ ├── create-deprecation-proxy.js │ ├── time-ranges.js │ ├── merge-options.js │ ├── log.js │ └── browser.js │ ├── plugins.js │ ├── base-styles.js │ ├── loading-spinner.js │ ├── close-button.js │ ├── control-bar │ ├── volume-control │ │ ├── volume-level.js │ │ └── volume-control.js │ ├── time-controls │ │ ├── time-divider.js │ │ ├── current-time-display.js │ │ ├── remaining-time-display.js │ │ └── duration-display.js │ ├── spacer-controls │ │ ├── spacer.js │ │ └── custom-control-spacer.js │ ├── progress-control │ │ ├── progress-control.js │ │ ├── tooltip-progress-bar.js │ │ ├── play-progress-bar.js │ │ └── load-progress-bar.js │ ├── fullscreen-toggle.js │ ├── text-track-controls │ │ ├── subtitles-button.js │ │ ├── caption-settings-menu-item.js │ │ ├── off-text-track-menu-item.js │ │ ├── chapters-track-menu-item.js │ │ ├── text-track-button.js │ │ ├── captions-button.js │ │ └── descriptions-button.js │ ├── live-display.js │ ├── playback-rate-menu │ │ └── playback-rate-menu-item.js │ ├── play-toggle.js │ ├── control-bar.js │ └── mute-toggle.js │ ├── tracks │ ├── text-track-enums.js │ ├── html-track-element-list.js │ ├── text-track-cue-list.js │ └── text-track-list-converter.js │ ├── big-play-button.js │ ├── event-target.js │ ├── error-display.js │ ├── popup │ ├── popup.js │ └── popup-button.js │ ├── tech │ └── loader.js │ ├── fullscreen-api.js │ ├── extend.js │ ├── menu │ └── menu-item.js │ └── media-error.js ├── .npmignore ├── Gruntfile.js ├── test ├── unit │ ├── utils │ │ ├── to-title-case.test.js │ │ ├── fn.test.js │ │ ├── merge-options.test.js │ │ ├── format-time.test.js │ │ ├── time-ranges.test.js │ │ ├── create-deprecation-proxy.test.js │ │ └── log.test.js │ ├── extend.test.js │ ├── setup.test.js │ ├── button.test.js │ ├── clickable-component.test.js │ ├── close-button.test.js │ ├── tech │ │ ├── tech-faker.js │ │ └── flash-rtmp.test.js │ ├── menu.test.js │ └── tracks │ │ ├── html-track-element-list.test.js │ │ └── html-track-element.test.js ├── index.html └── globals-shim.js ├── .editorconfig ├── composer.json ├── .gitignore ├── bower.json ├── LICENSE ├── component.json ├── PULL_REQUEST_TEMPLATE.md ├── docs ├── examples │ ├── shared │ │ └── example-captions.vtt │ ├── elephantsdream │ │ ├── chapters.en.vtt │ │ └── index.html │ └── simple-embed │ │ └── index.html ├── guides │ ├── removing-players.md │ └── api.md └── index.md ├── ISSUE_TEMPLATE.md ├── .jshintrc ├── lang ├── zh-CN.json ├── zh-TW.json ├── ko.json ├── ja.json ├── fa.json ├── cs.json ├── vi.json ├── pt-BR.json ├── ba.json ├── hr.json ├── sr.json ├── fi.json ├── hu.json ├── nn.json ├── sv.json ├── uk.json ├── nb.json ├── da.json ├── ru.json ├── bg.json ├── nl.json ├── it.json ├── ca.json ├── fr.json ├── es.json ├── tr.json ├── ar.json ├── pl.json ├── en.json ├── de.json └── el.json ├── sandbox ├── plugin.html.example ├── index.html.example ├── icons.html.example └── descriptions.html.example └── .travis.yml /src/css/vjs.scss: -------------------------------------------------------------------------------- 1 | $icon-codepoints: true; 2 | 3 | @import "video-js"; 4 | -------------------------------------------------------------------------------- /src/css/_variables.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: 'font' !default; 2 | $icon-codepoints: false !default; 3 | -------------------------------------------------------------------------------- /src/css/vjs-cdn.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: '//vjs.zencdn.net/font/1.5.1'; 2 | 3 | @import 'video-js'; 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Exclude everything but the contents of the dist directory. 2 | **/* 3 | !dist/** 4 | !src/css/** 5 | -------------------------------------------------------------------------------- /src/css/components/_captions.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-captions-button { 2 | @extend .vjs-icon-captions; 3 | } 4 | -------------------------------------------------------------------------------- /src/css/components/_control-spacer.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-custom-control-spacer { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /src/css/components/_subtitles.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-subtitles-button { 2 | @extend .vjs-icon-subtitles; 3 | } 4 | -------------------------------------------------------------------------------- /src/css/components/_descriptions.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-descriptions-button { 2 | @extend .vjs-icon-audio-description; 3 | } 4 | -------------------------------------------------------------------------------- /src/css/components/_chapters.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-chapters-button { 2 | @extend .vjs-icon-chapters; 3 | } 4 | 5 | .vjs-chapters-button .vjs-menu ul { 6 | width: 24em; 7 | } 8 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | require('babel/register'); 2 | 3 | // Need to `require` a separate Grunt file so we can use ES6 syntax via 4 | // Babel's require hook. 5 | module.exports = function(grunt) { 6 | require('./build/grunt.js')(grunt); 7 | }; 8 | -------------------------------------------------------------------------------- /src/css/components/_close-button.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-control.vjs-close-button { 2 | @extend .vjs-icon-cancel; 3 | cursor: pointer; 4 | height: 3em; 5 | position: absolute; 6 | right: 0; 7 | top: 0.5em; 8 | z-index: 2; 9 | } 10 | -------------------------------------------------------------------------------- /src/css/components/_play-pause.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-play-control { 2 | cursor: pointer; 3 | @include flex(none); 4 | } 5 | .video-js .vjs-play-control { 6 | @extend .vjs-icon-play; 7 | } 8 | .video-js .vjs-play-control.vjs-playing { 9 | @extend .vjs-icon-pause; 10 | } 11 | -------------------------------------------------------------------------------- /test/unit/utils/to-title-case.test.js: -------------------------------------------------------------------------------- 1 | import toTitleCase from '../../../src/js/utils/to-title-case.js'; 2 | 3 | q.module('to-title-case'); 4 | 5 | test('should make a string start with an uppercase letter', function(){ 6 | var foo = toTitleCase('bar'); 7 | ok(foo === 'Bar'); 8 | }); 9 | -------------------------------------------------------------------------------- /.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/js/utils/guid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file guid.js 3 | * 4 | * Unique ID for an element or function 5 | * @type {Number} 6 | * @private 7 | */ 8 | let _guid = 1; 9 | 10 | /** 11 | * Get the next unique ID 12 | * 13 | * @return {String} 14 | * @function newGUID 15 | */ 16 | export function newGUID() { 17 | return _guid++; 18 | } 19 | -------------------------------------------------------------------------------- /test/unit/utils/fn.test.js: -------------------------------------------------------------------------------- 1 | import * as Fn from '../../../src/js/utils/fn.js'; 2 | 3 | q.module('fn'); 4 | 5 | test('should add context to a function', function(){ 6 | var newContext = { test: 'obj'}; 7 | var asdf = function(){ 8 | ok(this === newContext); 9 | }; 10 | var fdsa = Fn.bind(newContext, asdf); 11 | 12 | fdsa(); 13 | }); 14 | -------------------------------------------------------------------------------- /src/css/components/_fullscreen.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-fullscreen-control { 2 | cursor: pointer; 3 | @include flex(none); 4 | } 5 | .video-js .vjs-fullscreen-control { 6 | @extend .vjs-icon-fullscreen-enter; 7 | } 8 | // Switch to the exit icon when the player is in fullscreen 9 | .video-js.vjs-fullscreen .vjs-fullscreen-control { 10 | @extend .vjs-icon-fullscreen-exit; 11 | } 12 | -------------------------------------------------------------------------------- /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 | 6 | .vjs-modal-dialog .vjs-modal-dialog-content { 7 | @extend %fill-parent; 8 | 9 | font-size: 1.2em; // 12px 10 | line-height: 1.5; // 18px 11 | padding: 20px 24px; 12 | z-index: 1; 13 | } 14 | -------------------------------------------------------------------------------- /src/js/utils/to-title-case.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file to-title-case.js 3 | * 4 | * Uppercase the first letter of a string 5 | * 6 | * @param {String} string String to be uppercased 7 | * @return {String} 8 | * @private 9 | * @method toTitleCase 10 | */ 11 | function toTitleCase(string){ 12 | return string.charAt(0).toUpperCase() + string.slice(1); 13 | } 14 | 15 | export default toTitleCase; 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videojs/video.js", 3 | "description": "An HTML5 and Flash video player with a common API and skin for both.", 4 | "type": "library", 5 | "keywords": [ 6 | "videojs", 7 | "html5", 8 | "flash", 9 | "video", 10 | "player" 11 | ], 12 | "homepage": "http://www.videojs.com/", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /src/css/components/_playback-rate.scss: -------------------------------------------------------------------------------- 1 | // TODO: I feel like this should be a generic menu. Research later. 2 | .vjs-playback-rate .vjs-playback-rate-value { 3 | font-size: 1.5em; 4 | line-height: 2; 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | width: 100%; 9 | height: 100%; 10 | text-align: center; 11 | } 12 | 13 | .vjs-playback-rate .vjs-menu { 14 | width: 4em; 15 | left: 0em; 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/files/* 3 | build/temp/* 4 | docs/api/* 5 | dev.html 6 | projects 7 | .zenflow-log 8 | test/*.map 9 | .bunyipconfig.js 10 | .s3config.json 11 | 12 | node_modules 13 | npm-debug.log 14 | 15 | sandbox/* 16 | !sandbox/*.example 17 | 18 | *.swp 19 | *.swo 20 | 21 | *.orig 22 | 23 | *results.xml 24 | *.log 25 | 26 | test/coverage/* 27 | .coveralls.yml 28 | .sass-cache 29 | 30 | dist/* 31 | 32 | .idea/ 33 | -------------------------------------------------------------------------------- /src/css/components/_live.scss: -------------------------------------------------------------------------------- 1 | // We are assuming there is no progress bar and using the live display 2 | // to fill in the middle space. Live+DVR will need to adjust this. 3 | .video-js .vjs-live-control { 4 | @include display-flex(flex-start); 5 | @include flex(auto); 6 | font-size: 1em; 7 | line-height: 3em; 8 | } 9 | 10 | .vjs-no-flex .vjs-live-control { 11 | display: table-cell; 12 | width: auto; 13 | text-align: left; 14 | } 15 | -------------------------------------------------------------------------------- /src/js/plugins.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file plugins.js 3 | */ 4 | import Player from './player.js'; 5 | 6 | /** 7 | * The method for registering a video.js plugin 8 | * 9 | * @param {String} name The name of the plugin 10 | * @param {Function} init The function that is run when the player inits 11 | * @method plugin 12 | */ 13 | var plugin = function(name, init){ 14 | Player.prototype[name] = init; 15 | }; 16 | 17 | export default plugin; 18 | -------------------------------------------------------------------------------- /src/js/utils/stylesheet.js: -------------------------------------------------------------------------------- 1 | import document from 'global/document'; 2 | 3 | export let createStyleElement = function(className) { 4 | let style = document.createElement('style'); 5 | style.className = className; 6 | 7 | return style; 8 | }; 9 | 10 | export let setTextContent = function(el, content) { 11 | if (el.styleSheet) { 12 | el.styleSheet.cssText = content; 13 | } else { 14 | el.textContent = content; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/css/components/_button.scss: -------------------------------------------------------------------------------- 1 | .video-js button { 2 | background: none; 3 | border: none; 4 | color: inherit; 5 | display: inline-block; 6 | 7 | overflow: visible; // IE8 8 | font-size: inherit; // IE in general. WTF. 9 | line-height: inherit; 10 | text-transform: none; 11 | text-decoration: none; 12 | transition: none; 13 | 14 | -webkit-appearance: none; 15 | -moz-appearance: none; 16 | appearance: none; 17 | } 18 | -------------------------------------------------------------------------------- /src/css/components/_slider.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-slider { 2 | outline: 0; 3 | position: relative; 4 | cursor: pointer; 5 | padding: 0; 6 | margin: 0 0.45em 0 0.45em; 7 | 8 | @include background-color-with-alpha($secondary-background-color, $secondary-background-transparency); 9 | } 10 | 11 | .video-js .vjs-slider:focus { 12 | text-shadow: 0em 0em 1em rgba($primary-foreground-color, 1); 13 | 14 | @include box-shadow(0 0 1em $primary-foreground-color); 15 | } 16 | -------------------------------------------------------------------------------- /test/unit/extend.test.js: -------------------------------------------------------------------------------- 1 | import extendFn from '../../src/js/extend.js'; 2 | 3 | q.module('extend.js'); 4 | 5 | test('should add implicit parent constructor call', function(){ 6 | var superCalled = false; 7 | var Parent = function() { 8 | superCalled = true; 9 | }; 10 | var Child = extendFn(Parent, { 11 | foo: 'bar' 12 | }); 13 | var child = new Child(); 14 | ok(superCalled, 'super constructor called'); 15 | ok(child.foo, 'child properties set'); 16 | }); 17 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video.js", 3 | "description": "An HTML5 and Flash video player with a common API and skin for both.", 4 | "main": [ 5 | "dist/video.js", 6 | "dist/video-js.css" 7 | ], 8 | "moduleType": "es6", 9 | "keywords": [ 10 | "videojs", 11 | "html5", 12 | "flash", 13 | "video", 14 | "player" 15 | ], 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "test", 21 | "tests", 22 | "build" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/js/base-styles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file base-styles.js 3 | * 4 | * This code injects the required base styles in the head of the document. 5 | */ 6 | import window from 'global/window'; 7 | import document from 'global/document'; 8 | 9 | if (window.VIDEOJS_NO_BASE_THEME) return; 10 | 11 | const styles = '{{GENERATED_STYLES}}'; 12 | 13 | if (styles === '{{GENERATED'+'_STYLES}}'); 14 | 15 | const styleNode = document.createElement('style'); 16 | styleNode.innerHTML = styles; 17 | 18 | document.head.insertBefore(styleNode, document.head.firstChild); 19 | -------------------------------------------------------------------------------- /test/unit/setup.test.js: -------------------------------------------------------------------------------- 1 | import TestHelpers from './test-helpers.js'; 2 | 3 | q.module('Setup'); 4 | 5 | test('should set options from data-setup even if autoSetup is not called before initialisation', function(){ 6 | var el = TestHelpers.makeTag(); 7 | el.setAttribute('data-setup', '{"controls": true, "autoplay": false, "preload": "auto"}'); 8 | 9 | var player = TestHelpers.makePlayer({}, el); 10 | 11 | ok(player.options_['controls'] === true); 12 | ok(player.options_['autoplay'] === false); 13 | ok(player.options_['preload'] === 'auto'); 14 | }); 15 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | VideoJS Tests 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Brightcove, Inc. 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. -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video.js", 3 | "description": "An HTML5 and Flash video player with a common API and skin for both.", 4 | "version": "5.9.0", 5 | "keywords": [ 6 | "videojs", 7 | "html5", 8 | "flash", 9 | "video", 10 | "player" 11 | ], 12 | "scripts": ["dist/video-js/video.dev.js"], 13 | "styles": ["dist/video-js/video-js.css"], 14 | "files": ["dist/video-js/video-js.swf"], 15 | "fonts": [ 16 | "dist/video-js/font/vjs.eot", 17 | "dist/video-js/font/vjs.svg", 18 | "dist/video-js/font/vjs.ttf", 19 | "dist/video-js/font/vjs.woff" 20 | ], 21 | "main": "dist/video-js/video.dev.js" 22 | } 23 | -------------------------------------------------------------------------------- /test/unit/button.test.js: -------------------------------------------------------------------------------- 1 | import Button from '../../src/js/button.js'; 2 | import TestHelpers from './test-helpers.js'; 3 | 4 | q.module('Button'); 5 | 6 | test('should localize its text', function(){ 7 | expect(2); 8 | 9 | var player, testButton, el; 10 | 11 | player = TestHelpers.makePlayer({ 12 | 'language': 'es', 13 | 'languages': { 14 | 'es': { 15 | 'Play': 'Juego' 16 | } 17 | } 18 | }); 19 | 20 | testButton = new Button(player); 21 | testButton.controlText_ = 'Play'; 22 | el = testButton.createEl(); 23 | 24 | ok(el.nodeName.toLowerCase().match('button')); 25 | ok(el.innerHTML.match('Juego')); 26 | }); 27 | -------------------------------------------------------------------------------- /test/globals-shim.js: -------------------------------------------------------------------------------- 1 | import document from 'global/document'; 2 | import window from 'global/window'; 3 | import sinon from 'sinon'; 4 | 5 | window.q = QUnit; 6 | window.sinon = sinon; 7 | 8 | // There's nowhere we require completing xhr requests 9 | // and raynos/xhr doesn't want to make stubbing easy (Raynos/xhr#11) 10 | // so we need to stub XHR before the xhr module is included anywhere else. 11 | window.xhr = sinon.useFakeXMLHttpRequest(); 12 | 13 | // This may not be needed anymore, but double check before removing 14 | window.fixture = document.createElement('div'); 15 | window.fixture.id = 'qunit-fixture'; 16 | document.body.appendChild(window.fixture); 17 | -------------------------------------------------------------------------------- /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 | 6 | .vjs-error .vjs-error-display:before { 7 | color: #fff; 8 | content: 'X'; 9 | font-family: $text-font-family; 10 | font-size: 4em; 11 | left: 0; 12 | 13 | // In order to center the play icon vertically we need to set the line height 14 | // to the same as the button height 15 | line-height: 1; 16 | margin-top: -0.5em; 17 | position: absolute; 18 | text-shadow: 0.05em 0.05em 0.1em #000; 19 | text-align: center; // Needed for IE8 20 | top: 50%; 21 | vertical-align: middle; 22 | width: 100%; 23 | } 24 | -------------------------------------------------------------------------------- /src/css/_private-variables.scss: -------------------------------------------------------------------------------- 1 | // Text, icons, hover states 2 | $primary-foreground-color: #fff !default; 3 | 4 | // Control backgrounds (control bar, big play, menus) 5 | $primary-background-color: #2B333F !default; 6 | $primary-background-transparency: 0.7 !default; 7 | 8 | // Hover states, slider backgrounds 9 | $secondary-background-color: lighten($primary-background-color, 33%) !default; 10 | $secondary-background-transparency: 0.5 !default; 11 | 12 | $text-font-family: Arial, Helvetica, sans-serif !default; 13 | 14 | // Using the '--' naming for component-specific styles 15 | $big-play-button--width: 3em !default; 16 | $big-play-button--height: 1.5em !default; 17 | $big-play-button--transparency: 0.8 !default; 18 | -------------------------------------------------------------------------------- /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 | 7 | ## Specific Changes proposed 8 | Please list the specific changes involved in this pull request. 9 | 10 | ## Requirements Checklist 11 | - [ ] Feature implemented / Bug fixed 12 | - [ ] If necessary, more likely in a feature request than a bug fix 13 | - [ ] Unit Tests updated or fixed 14 | - [ ] Docs/guides updated 15 | - [ ] Example created ([starter template on JSBin](http://jsbin.com/axedog/edit?html,output)) 16 | - [ ] Reviewed by Two Core Contributors 17 | -------------------------------------------------------------------------------- /src/js/loading-spinner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file loading-spinner.js 3 | */ 4 | import Component from './component'; 5 | 6 | /* Loading Spinner 7 | ================================================================================ */ 8 | /** 9 | * Loading spinner for waiting events 10 | * 11 | * @extends Component 12 | * @class LoadingSpinner 13 | */ 14 | class LoadingSpinner extends Component { 15 | 16 | /** 17 | * Create the component's DOM element 18 | * 19 | * @method createEl 20 | */ 21 | createEl() { 22 | return super.createEl('div', { 23 | className: 'vjs-loading-spinner', 24 | dir: 'ltr' 25 | }); 26 | } 27 | } 28 | 29 | Component.registerComponent('LoadingSpinner', LoadingSpinner); 30 | export default LoadingSpinner; 31 | -------------------------------------------------------------------------------- /test/unit/utils/merge-options.test.js: -------------------------------------------------------------------------------- 1 | import mergeOptions from '../../../src/js/utils/merge-options.js'; 2 | 3 | q.module('merge-options'); 4 | 5 | test('should merge options objects', function(){ 6 | var ob1, ob2, ob3; 7 | 8 | ob1 = { 9 | a: true, 10 | b: { b1: true, b2: true, b3: true }, 11 | c: true 12 | }; 13 | 14 | ob2 = { 15 | // override value 16 | a: false, 17 | // merge sub-option values 18 | b: { b1: true, b2: false, b4: true }, 19 | // add new option 20 | d: true 21 | }; 22 | 23 | ob3 = mergeOptions(ob1, ob2); 24 | 25 | deepEqual(ob3, { 26 | a: false, 27 | b: { b1: true, b2: false, b3: true, b4: true }, 28 | c: true, 29 | d: true 30 | }, 'options objects merged correctly'); 31 | }); 32 | -------------------------------------------------------------------------------- /src/js/close-button.js: -------------------------------------------------------------------------------- 1 | import Button from './button'; 2 | import Component from './component'; 3 | 4 | /** 5 | * The `CloseButton` component is a button which fires a "close" event 6 | * when it is activated. 7 | * 8 | * @extends Button 9 | * @class CloseButton 10 | */ 11 | class CloseButton extends Button { 12 | 13 | constructor(player, options) { 14 | super(player, options); 15 | this.controlText(options && options.controlText || this.localize('Close')); 16 | } 17 | 18 | buildCSSClass() { 19 | return `vjs-close-button ${super.buildCSSClass()}`; 20 | } 21 | 22 | handleClick() { 23 | this.trigger({type: 'close', bubbles: false}); 24 | } 25 | } 26 | 27 | Component.registerComponent('CloseButton', CloseButton); 28 | export default CloseButton; 29 | -------------------------------------------------------------------------------- /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 | * @param {Player|Object} player 10 | * @param {Object=} options 11 | * @extends Component 12 | * @class VolumeLevel 13 | */ 14 | class VolumeLevel extends Component { 15 | 16 | /** 17 | * Create the component's DOM element 18 | * 19 | * @return {Element} 20 | * @method createEl 21 | */ 22 | createEl() { 23 | return super.createEl('div', { 24 | className: 'vjs-volume-level', 25 | innerHTML: '' 26 | }); 27 | } 28 | 29 | } 30 | 31 | Component.registerComponent('VolumeLevel', VolumeLevel); 32 | export default VolumeLevel; 33 | -------------------------------------------------------------------------------- /src/css/components/menu/_menu-popup.scss: -------------------------------------------------------------------------------- 1 | .vjs-menu-button-popup .vjs-menu { 2 | display: none; 3 | position: absolute; 4 | bottom: 0; 5 | width: 10em; 6 | left: -3em; // (Width of vjs-menu - width of button) / 2 7 | height: 0em; 8 | margin-bottom: 1.5em; 9 | border-top-color: rgba($primary-background-color, $primary-background-transparency); // Same as ul background 10 | } 11 | 12 | // Button Pop-up Menu 13 | .vjs-menu-button-popup .vjs-menu .vjs-menu-content { 14 | @include background-color-with-alpha($primary-background-color, $primary-background-transparency); 15 | 16 | position: absolute; 17 | width: 100%; 18 | bottom: 1.5em; // Same bottom as vjs-menu border-top 19 | max-height: 15em; 20 | } 21 | 22 | .vjs-workinghover .vjs-menu-button-popup:hover .vjs-menu, 23 | .vjs-menu-button-popup .vjs-menu.vjs-lock-showing { 24 | display: block; 25 | } 26 | -------------------------------------------------------------------------------- /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 | * @param {Player|Object} player 11 | * @param {Object=} options 12 | * @extends Component 13 | * @class TimeDivider 14 | */ 15 | class TimeDivider extends Component { 16 | 17 | /** 18 | * Create the component's DOM element 19 | * 20 | * @return {Element} 21 | * @method createEl 22 | */ 23 | createEl() { 24 | return super.createEl('div', { 25 | className: 'vjs-time-control vjs-time-divider', 26 | innerHTML: '
/
' 27 | }); 28 | } 29 | 30 | } 31 | 32 | Component.registerComponent('TimeDivider', TimeDivider); 33 | export default TimeDivider; 34 | -------------------------------------------------------------------------------- /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 | display: none; 13 | } 14 | 15 | // We need the extra specificity that referencing .vjs-no-flex provides. 16 | .video-js .vjs-current-time, 17 | .vjs-no-flex .vjs-current-time { 18 | display: none; 19 | } 20 | 21 | .video-js .vjs-duration, 22 | .vjs-no-flex .vjs-duration { 23 | display: none; 24 | } 25 | 26 | .vjs-time-divider { 27 | display: none; 28 | line-height: 3em; 29 | } 30 | 31 | .vjs-live .vjs-time-divider { 32 | // Already the default, but we want to ensure when the player is live 33 | // this hides in the same way as the other time controls for other skins 34 | display: none; 35 | } 36 | -------------------------------------------------------------------------------- /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 ] -------------------------------------------------------------------------------- /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/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 | outline: none; 6 | position: relative; 7 | text-align: center; 8 | margin: 0; 9 | padding: 0; 10 | height: 100%; 11 | width: 4em; 12 | @include flex(none); 13 | 14 | &:before { 15 | font-size: 1.8em; 16 | line-height: 1.67; 17 | 18 | @extend %icon-default; 19 | } 20 | } 21 | 22 | // Replacement for focus outline 23 | .video-js .vjs-control:focus:before, 24 | .video-js .vjs-control:hover:before, 25 | .video-js .vjs-control:focus { 26 | text-shadow: 0em 0em 1em rgba($primary-foreground-color, 1); 27 | } 28 | 29 | // Hide control text visually, but have it available for screenreaders 30 | .video-js .vjs-control-text { 31 | @include hide-visually; 32 | } 33 | 34 | // IE 8 + 9 Support 35 | .vjs-no-flex .vjs-control { 36 | display: table-cell; 37 | vertical-align: middle; 38 | } 39 | -------------------------------------------------------------------------------- /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 | * @class Spacer 12 | */ 13 | class Spacer extends Component { 14 | 15 | /** 16 | * Allow sub components to stack CSS class names 17 | * 18 | * @return {String} The constructed class name 19 | * @method buildCSSClass 20 | */ 21 | buildCSSClass() { 22 | return `vjs-spacer ${super.buildCSSClass()}`; 23 | } 24 | 25 | /** 26 | * Create the component's DOM element 27 | * 28 | * @return {Element} 29 | * @method createEl 30 | */ 31 | createEl() { 32 | return super.createEl('div', { 33 | className: this.buildCSSClass() 34 | }); 35 | } 36 | } 37 | 38 | Component.registerComponent('Spacer', Spacer); 39 | 40 | export default Spacer; 41 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | Briefly describe the issue. 3 | Include a [reduced test case](https://css-tricks.com/reduced-test-cases/), we have a [starter template](http://jsbin.com/axedog/edit?html,output) on JSBin you can use. 4 | 5 | ## Steps to reproduce 6 | Explain in detail the exact steps necessary to reproduce the issue. 7 | 8 | 1. 9 | 2. 10 | 3. 11 | 12 | ## Results 13 | ### Expected 14 | Please describe what you expected to see. 15 | 16 | ### Actual 17 | Please describe what actually happened. 18 | 19 | ### Error output 20 | If there are any errors at all, please include them here. 21 | 22 | ## Additional Information 23 | Please include any additional information necessary here. Including the following: 24 | ### versions 25 | #### videojs 26 | what version of videojs does this occur with? 27 | #### browsers 28 | what browser are affected? 29 | #### OSes 30 | what platforms (operating systems and devices) are affected? 31 | ### plugins 32 | are any videojs plugins being used on the page? If so, please list them below. 33 | -------------------------------------------------------------------------------- /src/js/control-bar/progress-control/progress-control.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file progress-control.js 3 | */ 4 | import Component from '../../component.js'; 5 | import SeekBar from './seek-bar.js'; 6 | import MouseTimeDisplay from './mouse-time-display.js'; 7 | 8 | /** 9 | * The Progress Control component contains the seek bar, load progress, 10 | * and play progress 11 | * 12 | * @param {Player|Object} player 13 | * @param {Object=} options 14 | * @extends Component 15 | * @class ProgressControl 16 | */ 17 | class ProgressControl extends Component { 18 | 19 | /** 20 | * Create the component's DOM element 21 | * 22 | * @return {Element} 23 | * @method createEl 24 | */ 25 | createEl() { 26 | return super.createEl('div', { 27 | className: 'vjs-progress-control vjs-control' 28 | }); 29 | } 30 | } 31 | 32 | ProgressControl.prototype.options_ = { 33 | children: [ 34 | 'seekBar' 35 | ] 36 | }; 37 | 38 | Component.registerComponent('ProgressControl', ProgressControl); 39 | export default ProgressControl; 40 | -------------------------------------------------------------------------------- /src/js/utils/buffer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file buffer.js 3 | */ 4 | import { createTimeRange } from './time-ranges.js'; 5 | 6 | /** 7 | * Compute how much your video has been buffered 8 | * 9 | * @param {Object} Buffered object 10 | * @param {Number} Total duration 11 | * @return {Number} Percent buffered of the total duration 12 | * @private 13 | * @function bufferedPercent 14 | */ 15 | export function bufferedPercent(buffered, duration) { 16 | var bufferedDuration = 0, 17 | start, end; 18 | 19 | if (!duration) { 20 | return 0; 21 | } 22 | 23 | if (!buffered || !buffered.length) { 24 | buffered = createTimeRange(0, 0); 25 | } 26 | 27 | for (let i = 0; i < buffered.length; i++){ 28 | start = buffered.start(i); 29 | end = buffered.end(i); 30 | 31 | // buffered end can be bigger than duration by a very small fraction 32 | if (end > duration) { 33 | end = duration; 34 | } 35 | 36 | bufferedDuration += end - start; 37 | } 38 | 39 | return bufferedDuration / duration; 40 | } 41 | -------------------------------------------------------------------------------- /src/js/tracks/text-track-enums.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file text-track-enums.js 3 | */ 4 | 5 | /** 6 | * https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode 7 | * 8 | * enum TextTrackMode { "disabled", "hidden", "showing" }; 9 | */ 10 | const TextTrackMode = { 11 | disabled: 'disabled', 12 | hidden: 'hidden', 13 | showing: 'showing' 14 | }; 15 | 16 | /** 17 | * https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackkind 18 | * 19 | * enum TextTrackKind { 20 | * "subtitles", 21 | * "captions", 22 | * "descriptions", 23 | * "chapters", 24 | * "metadata" 25 | * }; 26 | */ 27 | const TextTrackKind = { 28 | subtitles: 'subtitles', 29 | captions: 'captions', 30 | descriptions: 'descriptions', 31 | chapters: 'chapters', 32 | metadata: 'metadata' 33 | }; 34 | 35 | /* jshint ignore:start */ 36 | // we ignore jshint here because it does not see 37 | // TextTrackMode or TextTrackKind as defined here somehow... 38 | export { TextTrackMode, TextTrackKind }; 39 | /* jshint ignore:end */ 40 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "evil" : true, 3 | "validthis": true, 4 | "node" : true, 5 | "debug" : true, 6 | "boss" : true, 7 | "expr" : true, 8 | "eqnull" : true, 9 | "quotmark" : "single", 10 | "sub" : true, 11 | "trailing" : true, 12 | "undef" : true, 13 | "laxbreak" : true, 14 | "esnext" : true, 15 | "eqeqeq" : true, 16 | "predef" : [ 17 | "_V_", 18 | "goog", 19 | "console", 20 | 21 | "require", 22 | "define", 23 | "module", 24 | "exports", 25 | "process", 26 | 27 | "q", 28 | "asyncTest", 29 | "deepEqual", 30 | "equal", 31 | "expect", 32 | "module", 33 | "notDeepEqual", 34 | "notEqual", 35 | "notStrictEqual", 36 | "ok", 37 | "throws", 38 | "QUnit", 39 | "raises", 40 | "start", 41 | "stop", 42 | "strictEqual", 43 | "test", 44 | "throws", 45 | "sinon" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/css/components/_text-track.scss: -------------------------------------------------------------------------------- 1 | // Emulated tracks 2 | .vjs-text-track-display { 3 | position: absolute; 4 | bottom: 3em; 5 | left: 0; 6 | right: 0; 7 | top: 0; 8 | pointer-events: none; 9 | } 10 | 11 | // Move captions down when controls aren't being shown 12 | .video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display { 13 | bottom: 1em; 14 | } 15 | 16 | // Individual tracks 17 | .video-js .vjs-text-track { 18 | font-size: 1.4em; 19 | text-align: center; 20 | margin-bottom: 0.1em; 21 | // Transparent black background, or fallback to all black (oldIE) 22 | @include background-color-with-alpha(#000, 0.5); 23 | } 24 | 25 | .vjs-subtitles { color: #fff; } // Subtitles are white 26 | .vjs-captions { color: #fc6; } // Captions are yellow 27 | .vjs-tt-cue { display: block; } 28 | 29 | // Native tracks 30 | video::-webkit-media-text-track-display { 31 | @include transform(translateY(-3em)); 32 | } 33 | 34 | // Move captions down when controls aren't being shown 35 | .video-js.vjs-user-inactive.vjs-playing video::-webkit-media-text-track-display { 36 | @include transform(translateY(-1.5em)); 37 | } 38 | -------------------------------------------------------------------------------- /lang/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "播放", 3 | "Pause": "暂停", 4 | "Current Time": "当前时间", 5 | "Duration Time": "时长", 6 | "Remaining Time": "剩余时间", 7 | "Stream Type": "媒体流类型", 8 | "LIVE": "直播", 9 | "Loaded": "加载完毕", 10 | "Progress": "进度", 11 | "Fullscreen": "全屏", 12 | "Non-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 | "The media is encrypted and we do not have the keys to decrypt it.": "视频已加密,无法解密。" 27 | } 28 | -------------------------------------------------------------------------------- /lang/zh-TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "播放", 3 | "Pause": "暫停", 4 | "Current Time": "目前時間", 5 | "Duration Time": "總共時間", 6 | "Remaining Time": "剩餘時間", 7 | "Stream Type": "串流類型", 8 | "LIVE": "直播", 9 | "Loaded": "載入完畢", 10 | "Progress": "進度", 11 | "Fullscreen": "全螢幕", 12 | "Non-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 | "The media is encrypted and we do not have the keys to decrypt it.": "影片已加密,無法解密。" 27 | } 28 | 29 | -------------------------------------------------------------------------------- /lang/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "재생", 3 | "Pause": "일시중지", 4 | "Current Time": "현재 시간", 5 | "Duration Time": "지정 기간", 6 | "Remaining Time": "남은 시간", 7 | "Stream Type": "스트리밍 유형", 8 | "LIVE": "라이브", 9 | "Loaded": "로드됨", 10 | "Progress": "진행", 11 | "Fullscreen": "전체 화면", 12 | "Non-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 | -------------------------------------------------------------------------------- /src/js/control-bar/fullscreen-toggle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file fullscreen-toggle.js 3 | */ 4 | import Button from '../button.js'; 5 | import Component from '../component.js'; 6 | 7 | /** 8 | * Toggle fullscreen video 9 | * 10 | * @extends Button 11 | * @class FullscreenToggle 12 | */ 13 | class FullscreenToggle extends Button { 14 | 15 | /** 16 | * Allow sub components to stack CSS class names 17 | * 18 | * @return {String} The constructed class name 19 | * @method buildCSSClass 20 | */ 21 | buildCSSClass() { 22 | return `vjs-fullscreen-control ${super.buildCSSClass()}`; 23 | } 24 | 25 | /** 26 | * Handles click for full screen 27 | * 28 | * @method handleClick 29 | */ 30 | handleClick() { 31 | if (!this.player_.isFullscreen()) { 32 | this.player_.requestFullscreen(); 33 | this.controlText('Non-Fullscreen'); 34 | } else { 35 | this.player_.exitFullscreen(); 36 | this.controlText('Fullscreen'); 37 | } 38 | } 39 | 40 | } 41 | 42 | FullscreenToggle.prototype.controlText_ = 'Fullscreen'; 43 | 44 | Component.registerComponent('FullscreenToggle', FullscreenToggle); 45 | export default FullscreenToggle; 46 | -------------------------------------------------------------------------------- /lang/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "再生", 3 | "Pause": "一時停止", 4 | "Current Time": "現在の時間", 5 | "Duration Time": "長さ", 6 | "Remaining Time": "残りの時間", 7 | "Stream Type": "ストリームの種類", 8 | "LIVE": "ライブ", 9 | "Loaded": "ロード済み", 10 | "Progress": "進行状況", 11 | "Fullscreen": "フルスクリーン", 12 | "Non-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 | -------------------------------------------------------------------------------- /src/js/big-play-button.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file big-play-button.js 3 | */ 4 | import Button from './button.js'; 5 | import Component from './component.js'; 6 | 7 | /** 8 | * Initial play button. Shows before the video has played. The hiding of the 9 | * big play button is done via CSS and player states. 10 | * 11 | * @param {Object} player Main Player 12 | * @param {Object=} options Object of option names and values 13 | * @extends Button 14 | * @class BigPlayButton 15 | */ 16 | class BigPlayButton extends Button { 17 | 18 | constructor(player, options) { 19 | super(player, options); 20 | } 21 | 22 | /** 23 | * Allow sub components to stack CSS class names 24 | * 25 | * @return {String} The constructed class name 26 | * @method buildCSSClass 27 | */ 28 | buildCSSClass() { 29 | return 'vjs-big-play-button'; 30 | } 31 | 32 | /** 33 | * Handles click for play 34 | * 35 | * @method handleClick 36 | */ 37 | handleClick() { 38 | this.player_.play(); 39 | } 40 | 41 | } 42 | 43 | BigPlayButton.prototype.controlText_ = 'Play Video'; 44 | 45 | Component.registerComponent('BigPlayButton', BigPlayButton); 46 | export default BigPlayButton; 47 | -------------------------------------------------------------------------------- /src/css/components/_poster.scss: -------------------------------------------------------------------------------- 1 | .vjs-poster { 2 | display: inline-block; 3 | vertical-align: middle; 4 | background-repeat: no-repeat; 5 | background-position: 50% 50%; 6 | background-size: contain; 7 | cursor: pointer; 8 | margin: 0; 9 | padding: 0; 10 | position: absolute; 11 | top: 0; 12 | right: 0; 13 | bottom: 0; 14 | left: 0; 15 | height: 100%; 16 | } 17 | 18 | // Used for IE8 fallback 19 | .vjs-poster img { 20 | display: block; 21 | vertical-align: middle; 22 | margin: 0 auto; 23 | max-height: 100%; 24 | padding: 0; 25 | width: 100%; 26 | } 27 | 28 | // Hide the poster after the video has started playing 29 | .vjs-has-started .vjs-poster { 30 | display: none; 31 | } 32 | 33 | // Don't hide the poster if we're playing audio 34 | .vjs-audio.vjs-has-started .vjs-poster { 35 | display: block; 36 | } 37 | 38 | // Hide the poster when controls are disabled because it's clickable 39 | // and the native poster can take over 40 | .vjs-controls-disabled .vjs-poster { 41 | display: none; 42 | } 43 | 44 | // Hide the poster when native controls are used otherwise it covers them 45 | .vjs-using-native-controls .vjs-poster { 46 | display: none; 47 | } 48 | -------------------------------------------------------------------------------- /lang/fa.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "پخش", 3 | "Pause": "وقفه", 4 | "Current Time": "زمان کنونی", 5 | "Duration Time": "مدت زمان", 6 | "Remaining Time": "زمان باقیمانده", 7 | "Stream Type": "نوع استریم", 8 | "LIVE": "زنده", 9 | "Loaded": "فراخوانی شده", 10 | "Progress": "پیشرفت", 11 | "Fullscreen": "تمام صفحه", 12 | "Non-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 | -------------------------------------------------------------------------------- /src/js/control-bar/text-track-controls/subtitles-button.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file subtitles-button.js 3 | */ 4 | import TextTrackButton from './text-track-button.js'; 5 | import Component from '../../component.js'; 6 | 7 | /** 8 | * The button component for toggling and selecting subtitles 9 | * 10 | * @param {Object} player Player object 11 | * @param {Object=} options Object of option names and values 12 | * @param {Function=} ready Ready callback function 13 | * @extends TextTrackButton 14 | * @class SubtitlesButton 15 | */ 16 | class SubtitlesButton extends TextTrackButton { 17 | 18 | constructor(player, options, ready){ 19 | super(player, options, ready); 20 | this.el_.setAttribute('aria-label','Subtitles Menu'); 21 | } 22 | 23 | /** 24 | * Allow sub components to stack CSS class names 25 | * 26 | * @return {String} The constructed class name 27 | * @method buildCSSClass 28 | */ 29 | buildCSSClass() { 30 | return `vjs-subtitles-button ${super.buildCSSClass()}`; 31 | } 32 | 33 | } 34 | 35 | SubtitlesButton.prototype.kind_ = 'subtitles'; 36 | SubtitlesButton.prototype.controlText_ = 'Subtitles'; 37 | 38 | Component.registerComponent('SubtitlesButton', SubtitlesButton); 39 | export default SubtitlesButton; 40 | -------------------------------------------------------------------------------- /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 | * @class CustomControlSpacer 12 | */ 13 | class CustomControlSpacer extends Spacer { 14 | 15 | /** 16 | * Allow sub components to stack CSS class names 17 | * 18 | * @return {String} The constructed class name 19 | * @method buildCSSClass 20 | */ 21 | buildCSSClass() { 22 | return `vjs-custom-control-spacer ${super.buildCSSClass()}`; 23 | } 24 | 25 | /** 26 | * Create the component's DOM element 27 | * 28 | * @return {Element} 29 | * @method createEl 30 | */ 31 | createEl() { 32 | let el = super.createEl({ 33 | className: this.buildCSSClass(), 34 | }); 35 | 36 | // No-flex/table-cell mode requires there be some content 37 | // in the cell to fill the remaining space of the table. 38 | el.innerHTML = ' '; 39 | return el; 40 | } 41 | } 42 | 43 | Component.registerComponent('CustomControlSpacer', CustomControlSpacer); 44 | export default CustomControlSpacer; 45 | -------------------------------------------------------------------------------- /test/unit/utils/format-time.test.js: -------------------------------------------------------------------------------- 1 | import formatTime from '../../../src/js/utils/format-time.js'; 2 | 3 | q.module('format-time'); 4 | 5 | test('should format time as a string', function(){ 6 | ok(formatTime(1) === '0:01'); 7 | ok(formatTime(10) === '0:10'); 8 | ok(formatTime(60) === '1:00'); 9 | ok(formatTime(600) === '10:00'); 10 | ok(formatTime(3600) === '1:00:00'); 11 | ok(formatTime(36000) === '10:00:00'); 12 | ok(formatTime(360000) === '100:00:00'); 13 | 14 | // Using guide should provide extra leading zeros 15 | ok(formatTime(1,1) === '0:01'); 16 | ok(formatTime(1,10) === '0:01'); 17 | ok(formatTime(1,60) === '0:01'); 18 | ok(formatTime(1,600) === '00:01'); 19 | ok(formatTime(1,3600) === '0:00:01'); 20 | // Don't do extra leading zeros for hours 21 | ok(formatTime(1,36000) === '0:00:01'); 22 | ok(formatTime(1,360000) === '0:00:01'); 23 | 24 | // Do not display negative time 25 | ok(formatTime(-1) === '0:00'); 26 | ok(formatTime(-1,3600) === '0:00:00'); 27 | }); 28 | 29 | test('should format invalid times as dashes', function(){ 30 | equal(formatTime(Infinity, 90), '-:-'); 31 | equal(formatTime(NaN), '-:-'); 32 | // equal(formatTime(NaN, 216000), '-:--:--'); 33 | equal(formatTime(10, Infinity), '0:00:10'); 34 | equal(formatTime(90, NaN), '1:30'); 35 | }); 36 | -------------------------------------------------------------------------------- /lang/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Přehrát", 3 | "Pause": "Pauza", 4 | "Current Time": "Aktuální čas", 5 | "Duration Time": "Doba trvání", 6 | "Remaining Time": "Zbývající čas", 7 | "Stream Type": "Stream Type", 8 | "LIVE": "ŽIVĚ", 9 | "Loaded": "Načteno", 10 | "Progress": "Stav", 11 | "Fullscreen": "Celá obrazovka", 12 | "Non-Fullscreen": "Zmenšená obrazovka", 13 | "Mute": "Ztlumit zvuk", 14 | "Unmute": "Přehrát zvuk", 15 | "Playback Rate": "Rychlost přehrávání", 16 | "Subtitles": "Titulky", 17 | "subtitles off": "Titulky vypnuty", 18 | "Captions": "Popisky", 19 | "captions off": "Popisky vypnuty", 20 | "Chapters": "Kapitoly", 21 | "You aborted the media playback": "Přehrávání videa je přerušeno.", 22 | "A network error caused the media download to fail part-way.": "Video nemohlo být načteno, kvůli chybě v síti.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video nemohlo být načteno, buď kvůli chybě serveru nebo sítě nebo proto, že daný formát není podporován.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Váš prohlížeč nepodporuje formát videa.", 25 | "No compatible source was found for this media.": "Špatně zadaný zdroj videa." 26 | } 27 | -------------------------------------------------------------------------------- /src/css/video-js.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "private-variables"; 3 | @import "utilities"; 4 | 5 | @if $icon-codepoints { 6 | @import "node_modules/videojs-font/scss/icons-codepoints"; 7 | } @else { 8 | @import "node_modules/videojs-font/scss/icons"; 9 | } 10 | 11 | @import "components/layout"; 12 | @import "components/big-play"; 13 | @import "components/button"; 14 | @import "components/close-button"; 15 | 16 | @import "components/menu/menu"; 17 | @import "components/menu/menu-popup"; 18 | @import "components/menu/menu-inline"; 19 | 20 | @import "components/control-bar"; 21 | @import "components/control"; 22 | @import "components/control-spacer"; 23 | 24 | @import "components/progress"; 25 | @import "components/slider"; 26 | 27 | @import "components/volume"; 28 | 29 | @import "components/poster"; 30 | @import "components/live"; 31 | @import "components/time"; 32 | @import "components/play-pause"; 33 | @import "components/text-track"; 34 | @import "components/fullscreen"; 35 | @import "components/playback-rate"; 36 | @import "components/error"; 37 | @import "components/loading"; 38 | @import "components/captions"; 39 | @import "components/chapters"; 40 | @import "components/descriptions"; 41 | @import "components/subtitles"; 42 | @import "components/adaptive"; 43 | @import "components/captions-settings"; 44 | @import "components/modal-dialog"; 45 | -------------------------------------------------------------------------------- /lang/vi.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Phát", 3 | "Pause": "Tạm dừng", 4 | "Current Time": "Thời gian hiện tại", 5 | "Duration Time": "Độ dài", 6 | "Remaining Time": "Thời gian còn lại", 7 | "Stream Type": "Kiểu Stream", 8 | "LIVE": "TRỰC TIẾP", 9 | "Loaded": "Đã tải", 10 | "Progress": "Tiến trình", 11 | "Fullscreen": "Toàn màn hình", 12 | "Non-Fullscreen": "Thoát toàn màn hình", 13 | "Mute": "Tắt tiếng", 14 | "Unmute": "Bật âm thanh", 15 | "Playback Rate": "Tốc độ phát", 16 | "Subtitles": "Phụ đề", 17 | "subtitles off": "Tắt phụ đề", 18 | "Captions": "Chú thích", 19 | "captions off": "Tắt chú thích", 20 | "Chapters": "Chương", 21 | "You aborted the media playback": "Bạn đã hủy việc phát media.", 22 | "A network error caused the media download to fail part-way.": "Một lỗi mạng dẫn đến việc tải media bị lỗi.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video không tải được, mạng hay server có lỗi hoặc định dạng không được hỗ trợ.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Phát media đã bị hủy do một sai lỗi hoặc media sử dụng những tính năng trình duyệt không hỗ trợ.", 25 | "No compatible source was found for this media.": "Không có nguồn tương thích cho media này." 26 | } 27 | -------------------------------------------------------------------------------- /src/js/utils/fn.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file fn.js 3 | */ 4 | import { newGUID } from './guid.js'; 5 | 6 | /** 7 | * Bind (a.k.a proxy or Context). A simple method for changing the context of a function 8 | * It also stores a unique id on the function so it can be easily removed from events 9 | * 10 | * @param {*} context The object to bind as scope 11 | * @param {Function} fn The function to be bound to a scope 12 | * @param {Number=} uid An optional unique ID for the function to be set 13 | * @return {Function} 14 | * @private 15 | * @method bind 16 | */ 17 | export const bind = function(context, fn, uid) { 18 | // Make sure the function has a unique ID 19 | if (!fn.guid) { fn.guid = newGUID(); } 20 | 21 | // Create the new function that changes the context 22 | let ret = function() { 23 | return fn.apply(context, arguments); 24 | }; 25 | 26 | // Allow for the ability to individualize this function 27 | // Needed in the case where multiple objects might share the same prototype 28 | // IF both items add an event listener with the same function, then you try to remove just one 29 | // it will remove both because they both have the same guid. 30 | // when using this, you need to use the bind method when you remove the listener as well. 31 | // currently used in text tracks 32 | ret.guid = (uid) ? uid + '_' + fn.guid : fn.guid; 33 | 34 | return ret; 35 | }; 36 | -------------------------------------------------------------------------------- /lang/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Tocar", 3 | "Pause": "Pause", 4 | "Current Time": "Tempo", 5 | "Duration Time": "Duração", 6 | "Remaining Time": "Tempo Restante", 7 | "Stream Type": "Tipo de Stream", 8 | "LIVE": "AO VIVO", 9 | "Loaded": "Carregado", 10 | "Progress": "Progresso", 11 | "Fullscreen": "Tela Cheia", 12 | "Non-Fullscreen": "Tela Normal", 13 | "Mute": "Mudo", 14 | "Unmute": "Habilitar Som", 15 | "Playback Rate": "Velocidade", 16 | "Subtitles": "Legendas", 17 | "subtitles off": "Sem Legendas", 18 | "Captions": "Anotações", 19 | "captions off": "Sem Anotações", 20 | "Chapters": "Capítulos", 21 | "You aborted the media playback": "Você parou a execução de vídeo.", 22 | "A network error caused the media download to fail part-way.": "Um erro na rede fez o vídeo parar parcialmente.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "O vídeo não pode ser carregado, ou porque houve um problema com sua rede ou pelo formato do vídeo não ser suportado.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "A Execução foi interrompida por um problema com o vídeo ou por seu navegador não dar suporte ao seu formato.", 25 | "No compatible source was found for this media.": "Não foi encontrada fonte de vídeo compatível." 26 | } 27 | -------------------------------------------------------------------------------- /lang/ba.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Pusti", 3 | "Pause": "Pauza", 4 | "Current Time": "Trenutno vrijeme", 5 | "Duration Time": "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 | "Non-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 Time": "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 | "Non-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/sr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Pusti", 3 | "Pause": "Pauza", 4 | "Current Time": "Trenutno vrijeme", 5 | "Duration Time": "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 | "Non-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 | -------------------------------------------------------------------------------- /docs/examples/simple-embed/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Video.js | HTML5 Video Player 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /lang/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Toisto", 3 | "Pause": "Tauko", 4 | "Current Time": "Tämänhetkinen aika", 5 | "Duration Time": "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 | "Non-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 | -------------------------------------------------------------------------------- /test/unit/clickable-component.test.js: -------------------------------------------------------------------------------- 1 | import ClickableComponent from '../../src/js/clickable-component.js'; 2 | import TestHelpers from './test-helpers.js'; 3 | 4 | q.module('ClickableComponent'); 5 | 6 | q.test('should create a div with role="button"', function(){ 7 | expect(2); 8 | 9 | let player = TestHelpers.makePlayer({}); 10 | 11 | let testClickableComponent = new ClickableComponent(player); 12 | let el = testClickableComponent.createEl(); 13 | 14 | equal(el.nodeName.toLowerCase(), 'div', 'the name of the element is "div"'); 15 | equal(el.getAttribute('role').toLowerCase(), 'button', 'the role of the element is "button"'); 16 | 17 | testClickableComponent.dispose(); 18 | player.dispose(); 19 | }); 20 | 21 | q.test('should be enabled/disabled', function(){ 22 | expect(3); 23 | 24 | let player = TestHelpers.makePlayer({}); 25 | 26 | let testClickableComponent = new ClickableComponent(player); 27 | 28 | equal(testClickableComponent.hasClass('vjs-disabled'), false, 'ClickableComponent defaults to enabled'); 29 | 30 | testClickableComponent.disable(); 31 | 32 | equal(testClickableComponent.hasClass('vjs-disabled'), true, 'ClickableComponent is disabled'); 33 | 34 | testClickableComponent.enable(); 35 | 36 | equal(testClickableComponent.hasClass('vjs-disabled'), false, 'ClickableComponent is enabled'); 37 | 38 | testClickableComponent.dispose(); 39 | player.dispose(); 40 | }); 41 | -------------------------------------------------------------------------------- /src/js/control-bar/live-display.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file live-display.js 3 | */ 4 | import Component from '../component'; 5 | import * as Dom from '../utils/dom.js'; 6 | 7 | /** 8 | * Displays the live indicator 9 | * TODO - Future make it click to snap to live 10 | * 11 | * @extends Component 12 | * @class LiveDisplay 13 | */ 14 | class LiveDisplay extends Component { 15 | 16 | constructor(player, options) { 17 | super(player, options); 18 | 19 | this.updateShowing(); 20 | this.on(this.player(), 'durationchange', this.updateShowing); 21 | } 22 | 23 | /** 24 | * Create the component's DOM element 25 | * 26 | * @return {Element} 27 | * @method createEl 28 | */ 29 | createEl() { 30 | var el = super.createEl('div', { 31 | className: 'vjs-live-control vjs-control' 32 | }); 33 | 34 | this.contentEl_ = Dom.createEl('div', { 35 | className: 'vjs-live-display', 36 | innerHTML: `${this.localize('Stream Type')}${this.localize('LIVE')}` 37 | }, { 38 | 'aria-live': 'off' 39 | }); 40 | 41 | el.appendChild(this.contentEl_); 42 | return el; 43 | } 44 | 45 | updateShowing() { 46 | if (this.player().duration() === Infinity) { 47 | this.show(); 48 | } else { 49 | this.hide(); 50 | } 51 | } 52 | 53 | } 54 | 55 | Component.registerComponent('LiveDisplay', LiveDisplay); 56 | export default LiveDisplay; 57 | -------------------------------------------------------------------------------- /lang/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Lejátszás", 3 | "Pause": "Szünet", 4 | "Current Time": "Aktuális időpont", 5 | "Duration Time": "Hossz", 6 | "Remaining Time": "Hátralévő idő", 7 | "Stream Type": "Adatfolyam típusa", 8 | "LIVE": "ÉLŐ", 9 | "Loaded": "Betöltve", 10 | "Progress": "Állapot", 11 | "Fullscreen": "Teljes képernyő", 12 | "Non-Fullscreen": "Normál méret", 13 | "Mute": "Némítás", 14 | "Unmute": "Némítás kikapcsolva", 15 | "Playback Rate": "Lejátszási sebesség", 16 | "Subtitles": "Feliratok", 17 | "subtitles off": "Feliratok kikapcsolva", 18 | "Captions": "Magyarázó szöveg", 19 | "captions off": "Magyarázó szöveg kikapcsolva", 20 | "Chapters": "Fejezetek", 21 | "You aborted the media playback": "Leállította a lejátszást", 22 | "A network error caused the media download to fail part-way.": "Hálózati hiba miatt a videó részlegesen töltődött le.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "A videó nem tölthető be hálózati vagy kiszolgálói hiba miatt, vagy a formátuma nem támogatott.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "A lejátszás adatsérülés miatt leállt, vagy a videó egyes tulajdonságait a böngészője nem támogatja.", 25 | "No compatible source was found for this media.": "Nincs kompatibilis forrás ehhez a videóhoz." 26 | } 27 | -------------------------------------------------------------------------------- /lang/nn.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Spel", 3 | "Pause": "Pause", 4 | "Current Time": "Aktuell tid", 5 | "Duration Time": "Varigheit", 6 | "Remaining Time": "Tid attende", 7 | "Stream Type": "Type straum", 8 | "LIVE": "DIREKTE", 9 | "Loaded": "Lasta inn", 10 | "Progress": "Status", 11 | "Fullscreen": "Fullskjerm", 12 | "Non-Fullscreen": "Stenga fullskjerm", 13 | "Mute": "Ljod av", 14 | "Unmute": "Ljod på", 15 | "Playback Rate": "Avspelingsrate", 16 | "Subtitles": "Teksting på", 17 | "subtitles off": "Teksting av", 18 | "Captions": "Teksting for høyrselshemma på", 19 | "captions off": "Teksting for høyrselshemma av", 20 | "Chapters": "Kapitel", 21 | "You aborted the media playback": "Du avbraut avspelinga.", 22 | "A network error caused the media download to fail part-way.": "Ein nettverksfeil avbraut nedlasting av videoen.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Videoen kunne ikkje lastas ned, på grunn av ein nettverksfeil eller serverfeil, eller av di formatet ikkje er stoda.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Videoavspelinga blei broten på grunn av øydelagde data eller av di videoen ville gjera noe som nettlesaren din ikkje stodar.", 25 | "No compatible source was found for this media.": "Fant ikke en kompatibel kilde for dette mediainnholdet." 26 | } 27 | -------------------------------------------------------------------------------- /lang/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Spela", 3 | "Pause": "Pausa", 4 | "Current Time": "Aktuell tid", 5 | "Duration Time": "Total tid", 6 | "Remaining Time": "Återstående tid", 7 | "Stream Type": "Strömningstyp", 8 | "LIVE": "LIVE", 9 | "Loaded": "Laddad", 10 | "Progress": "Förlopp", 11 | "Fullscreen": "Fullskärm", 12 | "Non-Fullscreen": "Ej fullskärm", 13 | "Mute": "Ljud av", 14 | "Unmute": "Ljud på", 15 | "Playback Rate": "Uppspelningshastighet", 16 | "Subtitles": "Text på", 17 | "subtitles off": "Text av", 18 | "Captions": "Text på", 19 | "captions off": "Text av", 20 | "Chapters": "Kapitel", 21 | "You aborted the media playback": "Du har avbrutit videouppspelningen.", 22 | "A network error caused the media download to fail part-way.": "Ett nätverksfel gjorde att nedladdningen av videon avbröts.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Det gick inte att ladda videon, antingen på grund av ett server- eller nätverksfel, eller för att formatet inte stöds.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Uppspelningen avbröts på grund av att videon är skadad, eller också för att videon använder funktioner som din webbläsare inte stöder.", 25 | "No compatible source was found for this media.": "Det gick inte att hitta någon kompatibel källa för den här videon." 26 | } 27 | -------------------------------------------------------------------------------- /lang/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Відтворити", 3 | "Pause": "Призупинити", 4 | "Current Time": "Поточний час", 5 | "Duration Time": "Тривалість", 6 | "Remaining Time": "Час, що залишився", 7 | "Stream Type": "Тип потоку", 8 | "LIVE": "НАЖИВО", 9 | "Loaded": "Завантаження", 10 | "Progress": "Прогрес", 11 | "Fullscreen": "Повноекранний режим", 12 | "Non-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 | -------------------------------------------------------------------------------- /src/js/event-target.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file event-target.js 3 | */ 4 | import * as Events from './utils/events.js'; 5 | 6 | var EventTarget = function() {}; 7 | 8 | EventTarget.prototype.allowedEvents_ = {}; 9 | 10 | EventTarget.prototype.on = function(type, fn) { 11 | // Remove the addEventListener alias before calling Events.on 12 | // so we don't get into an infinite type loop 13 | let ael = this.addEventListener; 14 | this.addEventListener = Function.prototype; 15 | Events.on(this, type, fn); 16 | this.addEventListener = ael; 17 | }; 18 | EventTarget.prototype.addEventListener = EventTarget.prototype.on; 19 | 20 | EventTarget.prototype.off = function(type, fn) { 21 | Events.off(this, type, fn); 22 | }; 23 | EventTarget.prototype.removeEventListener = EventTarget.prototype.off; 24 | 25 | EventTarget.prototype.one = function(type, fn) { 26 | Events.one(this, type, fn); 27 | }; 28 | 29 | EventTarget.prototype.trigger = function(event) { 30 | let type = event.type || event; 31 | 32 | if (typeof event === 'string') { 33 | event = { 34 | type: type 35 | }; 36 | } 37 | event = Events.fixEvent(event); 38 | 39 | if (this.allowedEvents_[type] && this['on' + type]) { 40 | this['on' + type](event); 41 | } 42 | 43 | Events.trigger(this, event); 44 | }; 45 | // The standard DOM EventTarget.dispatchEvent() is aliased to trigger() 46 | EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger; 47 | 48 | export default EventTarget; 49 | -------------------------------------------------------------------------------- /lang/nb.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Spill", 3 | "Pause": "Pause", 4 | "Current Time": "Aktuell tid", 5 | "Duration Time": "Varighet", 6 | "Remaining Time": "Gjenstående tid", 7 | "Stream Type": "Type strøm", 8 | "LIVE": "DIREKTE", 9 | "Loaded": "Lastet inn", 10 | "Progress": "Status", 11 | "Fullscreen": "Fullskjerm", 12 | "Non-Fullscreen": "Lukk fullskjerm", 13 | "Mute": "Lyd av", 14 | "Unmute": "Lyd på", 15 | "Playback Rate": "Avspillingsrate", 16 | "Subtitles": "Undertekst på", 17 | "subtitles off": "Undertekst av", 18 | "Captions": "Undertekst for hørselshemmede på", 19 | "captions off": "Undertekst for hørselshemmede av", 20 | "Chapters": "Kapitler", 21 | "You aborted the media playback": "Du avbrøt avspillingen.", 22 | "A network error caused the media download to fail part-way.": "En nettverksfeil avbrøt nedlasting av videoen.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Videoen kunne ikke lastes ned, på grunn av nettverksfeil eller serverfeil, eller fordi formatet ikke er støttet.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Videoavspillingen ble avbrudt på grunn av ødelagte data eller fordi videoen ville gjøre noe som nettleseren din ikke har støtte for.", 25 | "No compatible source was found for this media.": "Fant ikke en kompatibel kilde for dette mediainnholdet." 26 | } 27 | -------------------------------------------------------------------------------- /test/unit/close-button.test.js: -------------------------------------------------------------------------------- 1 | import CloseButton from '../../src/js/close-button'; 2 | import TestHelpers from './test-helpers'; 3 | 4 | q.module('CloseButton', { 5 | 6 | beforeEach: function() { 7 | this.player = TestHelpers.makePlayer(); 8 | this.btn = new CloseButton(this.player); 9 | }, 10 | 11 | afterEach: function() { 12 | this.player.dispose(); 13 | this.btn.dispose(); 14 | } 15 | }); 16 | 17 | q.test('should create the expected element', function(assert) { 18 | let elAssertions = TestHelpers.assertEl(assert, this.btn.el(), { 19 | tagName: 'button', 20 | classes: [ 21 | 'vjs-button', 22 | 'vjs-close-button', 23 | 'vjs-control' 24 | ] 25 | }); 26 | 27 | assert.expect(elAssertions.count + 1); 28 | elAssertions(); 29 | assert.strictEqual(this.btn.el().querySelector('.vjs-control-text').innerHTML, 'Close'); 30 | }); 31 | 32 | q.test('should allow setting the controlText_ property as an option', function(assert) { 33 | var text = 'OK!'; 34 | var btn = new CloseButton(this.player, {controlText: text}); 35 | 36 | assert.expect(1); 37 | assert.strictEqual(btn.controlText_, text, 'set the controlText_ property'); 38 | }); 39 | 40 | q.test('should trigger an event on activation', function(assert) { 41 | var spy = sinon.spy(); 42 | 43 | this.btn.on('close', spy); 44 | this.btn.trigger('click'); 45 | assert.expect(1); 46 | assert.strictEqual(spy.callCount, 1, 'the "close" event was triggered'); 47 | }); 48 | -------------------------------------------------------------------------------- /lang/da.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Afspil", 3 | "Pause": "Pause", 4 | "Current Time": "Aktuel tid", 5 | "Duration Time": "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 | "Non-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/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Воспроизвести", 3 | "Pause": "Приостановить", 4 | "Current Time": "Текущее время", 5 | "Duration Time": "Продолжительность", 6 | "Remaining Time": "Оставшееся время", 7 | "Stream Type": "Тип потока", 8 | "LIVE": "ОНЛАЙН", 9 | "Loaded": "Загрузка", 10 | "Progress": "Прогресс", 11 | "Fullscreen": "Полноэкранный режим", 12 | "Non-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 | -------------------------------------------------------------------------------- /src/js/control-bar/text-track-controls/caption-settings-menu-item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file caption-settings-menu-item.js 3 | */ 4 | import TextTrackMenuItem from './text-track-menu-item.js'; 5 | import Component from '../../component.js'; 6 | 7 | /** 8 | * The menu item for caption track settings menu 9 | * 10 | * @param {Player|Object} player 11 | * @param {Object=} options 12 | * @extends TextTrackMenuItem 13 | * @class CaptionSettingsMenuItem 14 | */ 15 | class CaptionSettingsMenuItem extends TextTrackMenuItem { 16 | 17 | constructor(player, options) { 18 | options['track'] = { 19 | 'kind': options['kind'], 20 | 'player': player, 21 | 'label': options['kind'] + ' settings', 22 | 'selectable': false, 23 | 'default': false, 24 | mode: 'disabled' 25 | }; 26 | 27 | // CaptionSettingsMenuItem has no concept of 'selected' 28 | options['selectable'] = false; 29 | 30 | super(player, options); 31 | this.addClass('vjs-texttrack-settings'); 32 | this.controlText(', opens ' + options['kind'] + ' settings dialog'); 33 | } 34 | 35 | /** 36 | * Handle click on menu item 37 | * 38 | * @method handleClick 39 | */ 40 | handleClick() { 41 | this.player().getChild('textTrackSettings').show(); 42 | this.player().getChild('textTrackSettings').el_.focus(); 43 | } 44 | 45 | } 46 | 47 | Component.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem); 48 | export default CaptionSettingsMenuItem; 49 | -------------------------------------------------------------------------------- /lang/bg.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Възпроизвеждане", 3 | "Pause": "Пауза", 4 | "Current Time": "Текущо време", 5 | "Duration Time": "Продължителност", 6 | "Remaining Time": "Оставащо време", 7 | "Stream Type": "Тип на потока", 8 | "LIVE": "НА ЖИВО", 9 | "Loaded": "Заредено", 10 | "Progress": "Прогрес", 11 | "Fullscreen": "Цял екран", 12 | "Non-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 | -------------------------------------------------------------------------------- /lang/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Afspelen", 3 | "Pause": "Pauze", 4 | "Current Time": "Huidige tijd", 5 | "Duration Time": "Looptijd", 6 | "Remaining Time": "Resterende tijd", 7 | "Stream Type": "Streamtype", 8 | "LIVE": "LIVE", 9 | "Loaded": "Geladen", 10 | "Progress": "Status", 11 | "Fullscreen": "Volledig scherm", 12 | "Non-Fullscreen": "Geen volledig scherm", 13 | "Mute": "Geluid uit", 14 | "Unmute": "Geluid aan", 15 | "Playback Rate": "Weergavesnelheid", 16 | "Subtitles": "Ondertiteling", 17 | "subtitles off": "Ondertiteling uit", 18 | "Captions": "Ondertiteling", 19 | "captions off": "Ondertiteling uit", 20 | "Chapters": "Hoofdstukken", 21 | "You aborted the media playback": "U hebt de mediaweergave afgebroken.", 22 | "A network error caused the media download to fail part-way.": "De mediadownload is mislukt door een netwerkfout.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "De media kon niet worden geladen, vanwege een server- of netwerkfout of doordat het formaat niet wordt ondersteund.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "De mediaweergave is afgebroken vanwege beschadigde data of het mediabestand gebruikt functies die niet door uw browser worden ondersteund.", 25 | "No compatible source was found for this media.": "Voor deze media is geen ondersteunde bron gevonden." 26 | } 27 | -------------------------------------------------------------------------------- /src/css/components/menu/_menu.scss: -------------------------------------------------------------------------------- 1 | .vjs-menu-button { 2 | cursor: pointer; 3 | } 4 | 5 | // Change cursor back to default if the menu button is disabled 6 | .vjs-menu-button.vjs-disabled { 7 | cursor: default; 8 | } 9 | 10 | // prevent menus from opening while disabled 11 | .vjs-workinghover .vjs-menu-button.vjs-disabled:hover .vjs-menu { 12 | display: none; 13 | } 14 | 15 | .vjs-menu .vjs-menu-content { 16 | display: block; 17 | padding: 0; margin: 0; 18 | overflow: auto; 19 | } 20 | 21 | // prevent menus from opening while scrubbing (FF, IE) 22 | .vjs-scrubbing .vjs-menu-button:hover .vjs-menu { 23 | display: none; 24 | } 25 | 26 | .vjs-menu li { 27 | list-style: none; 28 | margin: 0; 29 | padding: 0.2em 0; 30 | line-height: 1.4em; 31 | font-size: 1.2em; 32 | text-align: center; 33 | text-transform: lowercase; 34 | } 35 | 36 | .vjs-menu li:focus, 37 | .vjs-menu li:hover { 38 | outline: 0; 39 | @include background-color-with-alpha($secondary-background-color, $secondary-background-transparency); 40 | } 41 | 42 | .vjs-menu li.vjs-selected, 43 | .vjs-menu li.vjs-selected:focus, 44 | .vjs-menu li.vjs-selected:hover { 45 | background-color: $primary-foreground-color; 46 | color: $primary-background-color; 47 | } 48 | 49 | .vjs-menu li.vjs-menu-title { 50 | text-align: center; 51 | text-transform: uppercase; 52 | font-size: 1em; 53 | line-height: 2em; 54 | padding: 0; 55 | margin: 0 0 0.3em 0; 56 | font-weight: bold; 57 | cursor: default; 58 | } 59 | -------------------------------------------------------------------------------- /src/js/control-bar/volume-control/volume-control.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file volume-control.js 3 | */ 4 | import Component from '../../component.js'; 5 | 6 | // Required children 7 | import VolumeBar from './volume-bar.js'; 8 | 9 | /** 10 | * The component for controlling the volume level 11 | * 12 | * @param {Player|Object} player 13 | * @param {Object=} options 14 | * @extends Component 15 | * @class VolumeControl 16 | */ 17 | class VolumeControl extends Component { 18 | 19 | constructor(player, options){ 20 | super(player, options); 21 | 22 | // hide volume controls when they're not supported by the current tech 23 | if (player.tech_ && player.tech_['featuresVolumeControl'] === false) { 24 | this.addClass('vjs-hidden'); 25 | } 26 | this.on(player, 'loadstart', function(){ 27 | if (player.tech_['featuresVolumeControl'] === false) { 28 | this.addClass('vjs-hidden'); 29 | } else { 30 | this.removeClass('vjs-hidden'); 31 | } 32 | }); 33 | } 34 | 35 | /** 36 | * Create the component's DOM element 37 | * 38 | * @return {Element} 39 | * @method createEl 40 | */ 41 | createEl() { 42 | return super.createEl('div', { 43 | className: 'vjs-volume-control vjs-control' 44 | }); 45 | } 46 | 47 | } 48 | 49 | VolumeControl.prototype.options_ = { 50 | children: [ 51 | 'volumeBar' 52 | ] 53 | }; 54 | 55 | Component.registerComponent('VolumeControl', VolumeControl); 56 | export default VolumeControl; 57 | -------------------------------------------------------------------------------- /test/unit/utils/time-ranges.test.js: -------------------------------------------------------------------------------- 1 | import { createTimeRanges, createTimeRange } from '../../../src/js/utils/time-ranges.js'; 2 | 3 | q.module('time-ranges'); 4 | 5 | test('should export the deprecated createTimeRange function', function(){ 6 | equal(createTimeRange, createTimeRanges, 'createTimeRange is an alias to createTimeRanges'); 7 | }); 8 | 9 | test('should create a fake single timerange', function(assert){ 10 | var tr = createTimeRanges(0, 10); 11 | 12 | equal(tr.length, 1, 'length should be 1'); 13 | equal(tr.start(0), 0, 'works if start is called with valid index'); 14 | equal(tr.end(0), 10, 'works if end is called with with valid index'); 15 | assert.throws(()=>tr.start(1), /Failed to execute 'start'/, 'fails if start is called with an invalid index'); 16 | assert.throws(()=>tr.end(1), /Failed to execute 'end'/, 'fails if end is called with with an invalid index'); 17 | }); 18 | 19 | test('should create a fake multiple timerange', function(assert){ 20 | var tr = createTimeRanges([ 21 | [0, 10], 22 | [11, 20] 23 | ]); 24 | 25 | equal(tr.length, 2, 'length should equal 2'); 26 | equal(tr.start(1), 11, 'works if start is called with valid index'); 27 | equal(tr.end(1), 20, 'works if end is called with with valid index'); 28 | assert.throws(()=>tr.start(-1), /Failed to execute 'start'/, 'fails if start is called with an invalid index'); 29 | assert.throws(()=>tr.end(-1), /Failed to execute 'end'/, 'fails if end is called with with an invalid index'); 30 | }); 31 | -------------------------------------------------------------------------------- /src/js/control-bar/playback-rate-menu/playback-rate-menu-item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file playback-rate-menu-item.js 3 | */ 4 | import MenuItem from '../../menu/menu-item.js'; 5 | import Component from '../../component.js'; 6 | 7 | /** 8 | * The specific menu item type for selecting a playback rate 9 | * 10 | * @param {Player|Object} player 11 | * @param {Object=} options 12 | * @extends MenuItem 13 | * @class PlaybackRateMenuItem 14 | */ 15 | class PlaybackRateMenuItem extends MenuItem { 16 | 17 | constructor(player, options){ 18 | let label = options['rate']; 19 | let rate = parseFloat(label, 10); 20 | 21 | // Modify options for parent MenuItem class's init. 22 | options['label'] = label; 23 | options['selected'] = rate === 1; 24 | super(player, options); 25 | 26 | this.label = label; 27 | this.rate = rate; 28 | 29 | this.on(player, 'ratechange', this.update); 30 | } 31 | 32 | /** 33 | * Handle click on menu item 34 | * 35 | * @method handleClick 36 | */ 37 | handleClick() { 38 | super.handleClick(); 39 | this.player().playbackRate(this.rate); 40 | } 41 | 42 | /** 43 | * Update playback rate with selected rate 44 | * 45 | * @method update 46 | */ 47 | update() { 48 | this.selected(this.player().playbackRate() === this.rate); 49 | } 50 | 51 | } 52 | 53 | PlaybackRateMenuItem.prototype.contentElType = 'button'; 54 | 55 | Component.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem); 56 | export default PlaybackRateMenuItem; 57 | -------------------------------------------------------------------------------- /src/js/utils/format-time.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file format-time.js 3 | * 4 | * Format seconds as a time string, H:MM:SS or M:SS 5 | * Supplying a guide (in seconds) will force a number of leading zeros 6 | * to cover the length of the guide 7 | * 8 | * @param {Number} seconds Number of seconds to be turned into a string 9 | * @param {Number} guide Number (in seconds) to model the string after 10 | * @return {String} Time formatted as H:MM:SS or M:SS 11 | * @private 12 | * @function formatTime 13 | */ 14 | function formatTime(seconds, guide=seconds) { 15 | seconds = seconds < 0 ? 0 : seconds; 16 | let s = Math.floor(seconds % 60); 17 | let m = Math.floor(seconds / 60 % 60); 18 | let h = Math.floor(seconds / 3600); 19 | const gm = Math.floor(guide / 60 % 60); 20 | const gh = Math.floor(guide / 3600); 21 | 22 | // handle invalid times 23 | if (isNaN(seconds) || seconds === Infinity) { 24 | // '-' is false for all relational operators (e.g. <, >=) so this setting 25 | // will add the minimum number of fields specified by the guide 26 | h = m = s = '-'; 27 | } 28 | 29 | // Check if we need to show hours 30 | h = (h > 0 || gh > 0) ? h + ':' : ''; 31 | 32 | // If hours are showing, we may need to add a leading zero. 33 | // Always show at least one digit of minutes. 34 | m = (((h || gm >= 10) && m < 10) ? '0' + m : m) + ':'; 35 | 36 | // Check if leading zero is need for seconds 37 | s = (s < 10) ? '0' + s : s; 38 | 39 | return h + m + s; 40 | } 41 | 42 | export default formatTime; 43 | -------------------------------------------------------------------------------- /lang/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Play", 3 | "Pause": "Pausa", 4 | "Current Time": "Orario attuale", 5 | "Duration Time": "Durata", 6 | "Remaining Time": "Tempo rimanente", 7 | "Stream Type": "Tipo del Streaming", 8 | "LIVE": "LIVE", 9 | "Loaded": "Caricato", 10 | "Progress": "Stato", 11 | "Fullscreen": "Schermo intero", 12 | "Non-Fullscreen": "Chiudi schermo intero", 13 | "Mute": "Muto", 14 | "Unmute": "Audio", 15 | "Playback Rate": "Tasso di riproduzione", 16 | "Subtitles": "Sottotitoli", 17 | "subtitles off": "Senza sottotitoli", 18 | "Captions": "Sottotitoli non udenti", 19 | "captions off": "Senza sottotitoli non udenti", 20 | "Chapters": "Capitolo", 21 | "You aborted the media playback": "La riproduzione del filmato è stata interrotta.", 22 | "A network error caused the media download to fail part-way.": "Il download del filmato è stato interrotto a causa di un problema rete.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Il filmato non può essere caricato a causa di un errore nel server o nella rete o perché il formato non viene supportato.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "La riproduzione del filmato è stata interrotta a causa di un file danneggiato o per l’utilizzo di impostazioni non supportate dal browser.", 25 | "No compatible source was found for this media.": "Non ci sono fonti compatibili per questo filmato." 26 | } 27 | -------------------------------------------------------------------------------- /lang/ca.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Reproducció", 3 | "Pause": "Pausa", 4 | "Current Time": "Temps reproduït", 5 | "Duration Time": "Durada total", 6 | "Remaining Time": "Temps restant", 7 | "Stream Type": "Tipus de seqüència", 8 | "LIVE": "EN DIRECTE", 9 | "Loaded": "Carregat", 10 | "Progress": "Progrés", 11 | "Fullscreen": "Pantalla completa", 12 | "Non-Fullscreen": "Pantalla no completa", 13 | "Mute": "Silencia", 14 | "Unmute": "Amb so", 15 | "Playback Rate": "Velocitat de reproducció", 16 | "Subtitles": "Subtítols", 17 | "subtitles off": "Subtítols desactivats", 18 | "Captions": "Llegendes", 19 | "captions off": "Llegendes desactivades", 20 | "Chapters": "Capítols", 21 | "You aborted the media playback": "Heu interromput la reproducció del vídeo.", 22 | "A network error caused the media download to fail part-way.": "Un error de la xarxa ha interromput la baixada del vídeo.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "No s'ha pogut carregar el vídeo perquè el servidor o la xarxa han fallat, o bé perquè el seu format no és compatible.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "La reproducció de vídeo s'ha interrumput per un problema de corrupció de dades o bé perquè el vídeo demanava funcions que el vostre navegador no ofereix.", 25 | "No compatible source was found for this media.": "No s'ha trobat cap font compatible amb el vídeo." 26 | } 27 | -------------------------------------------------------------------------------- /lang/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Lecture", 3 | "Pause": "Pause", 4 | "Current Time": "Temps actuel", 5 | "Duration Time": "Durée", 6 | "Remaining Time": "Temps restant", 7 | "Stream Type": "Type de flux", 8 | "LIVE": "EN DIRECT", 9 | "Loaded": "Chargé", 10 | "Progress": "Progression", 11 | "Fullscreen": "Plein écran", 12 | "Non-Fullscreen": "Fenêtré", 13 | "Mute": "Sourdine", 14 | "Unmute": "Son activé", 15 | "Playback Rate": "Vitesse de lecture", 16 | "Subtitles": "Sous-titres", 17 | "subtitles off": "Sous-titres désactivés", 18 | "Captions": "Sous-titres", 19 | "captions off": "Sous-titres désactivés", 20 | "Chapters": "Chapitres", 21 | "You aborted the media playback": "Vous avez interrompu la lecture de la vidéo.", 22 | "A network error caused the media download to fail part-way.": "Une erreur de réseau a interrompu le téléchargement de la vidéo.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Cette vidéo n'a pas pu être chargée, soit parce que le serveur ou le réseau a échoué ou parce que le format n'est pas reconnu.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "La lecture de la vidéo a été interrompue à cause d'un problème de corruption ou parce que la vidéo utilise des fonctionnalités non prises en charge par votre navigateur.", 25 | "No compatible source was found for this media.": "Aucune source compatible n'a été trouvée pour cette vidéo." 26 | } 27 | -------------------------------------------------------------------------------- /src/css/components/_adaptive.scss: -------------------------------------------------------------------------------- 1 | // When the player is absurdly tiny, display nothing but: 2 | // - Play button 3 | // - Fullscreen Button 4 | .video-js.vjs-layout-tiny:not(.vjs-fullscreen) { 5 | .vjs-custom-control-spacer { @include flex(auto); } 6 | &.vjs-no-flex .vjs-custom-control-spacer { width: auto; } 7 | 8 | .vjs-current-time, .vjs-time-divider, .vjs-duration, .vjs-remaining-time, 9 | .vjs-playback-rate, .vjs-progress-control, 10 | .vjs-mute-control, .vjs-volume-control, .vjs-volume-menu-button, 11 | .vjs-chapters-button, .vjs-captions-button, .vjs-subtitles-button { display: none; } 12 | } 13 | 14 | // When the player is x-small, display nothing but: 15 | // - Play button 16 | // - Progress bar 17 | // - Fullscreen Button 18 | .video-js.vjs-layout-x-small:not(.vjs-fullscreen) { 19 | .vjs-current-time, .vjs-time-divider, .vjs-duration, .vjs-remaining-time, 20 | .vjs-playback-rate, 21 | .vjs-mute-control, .vjs-volume-control, .vjs-volume-menu-button, 22 | .vjs-chapters-button, .vjs-captions-button, .vjs-subtitles-button { display: none; } 23 | } 24 | 25 | 26 | // When the player is small, display nothing but: 27 | // - Play button 28 | // - Progress bar 29 | // - Volume menu button 30 | // - Captions Button 31 | // - Fullscreen button 32 | .video-js.vjs-layout-small:not(.vjs-fullscreen) { 33 | .vjs-current-time, .vjs-time-divider, .vjs-duration, .vjs-remaining-time, 34 | .vjs-playback-rate, 35 | .vjs-mute-control, .vjs-volume-control, 36 | .vjs-chapters-button, .vjs-captions-button, .vjs-subtitles-button { display: none; } 37 | } 38 | -------------------------------------------------------------------------------- /lang/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Reproducción", 3 | "Pause": "Pausa", 4 | "Current Time": "Tiempo reproducido", 5 | "Duration Time": "Duración total", 6 | "Remaining Time": "Tiempo restante", 7 | "Stream Type": "Tipo de secuencia", 8 | "LIVE": "DIRECTO", 9 | "Loaded": "Cargado", 10 | "Progress": "Progreso", 11 | "Fullscreen": "Pantalla completa", 12 | "Non-Fullscreen": "Pantalla no completa", 13 | "Mute": "Silenciar", 14 | "Unmute": "No silenciado", 15 | "Playback Rate": "Velocidad de reproducción", 16 | "Subtitles": "Subtítulos", 17 | "subtitles off": "Subtítulos desactivados", 18 | "Captions": "Subtítulos especiales", 19 | "captions off": "Subtítulos especiales desactivados", 20 | "Chapters": "Capítulos", 21 | "You aborted the media playback": "Ha interrumpido la reproducción del vídeo.", 22 | "A network error caused the media download to fail part-way.": "Un error de red ha interrumpido la descarga del vídeo.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "No se ha podido cargar el vídeo debido a un fallo de red o del servidor o porque el formato es incompatible.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "La reproducción de vídeo se ha interrumpido por un problema de corrupción de datos o porque el vídeo precisa funciones que su navegador no ofrece.", 25 | "No compatible source was found for this media.": "No se ha encontrado ninguna fuente compatible con este vídeo." 26 | } 27 | -------------------------------------------------------------------------------- /src/js/error-display.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file error-display.js 3 | */ 4 | import Component from './component'; 5 | import ModalDialog from './modal-dialog'; 6 | 7 | import * as Dom from './utils/dom'; 8 | import mergeOptions from './utils/merge-options'; 9 | 10 | /** 11 | * Display that an error has occurred making the video unplayable. 12 | * 13 | * @extends ModalDialog 14 | * @class ErrorDisplay 15 | */ 16 | class ErrorDisplay extends ModalDialog { 17 | 18 | /** 19 | * Constructor for error display modal. 20 | * 21 | * @param {Player} player 22 | * @param {Object} [options] 23 | */ 24 | constructor(player, options) { 25 | super(player, options); 26 | this.on(player, 'error', this.open); 27 | } 28 | 29 | /** 30 | * Include the old class for backward-compatibility. 31 | * 32 | * This can be removed in 6.0. 33 | * 34 | * @method buildCSSClass 35 | * @deprecated 36 | * @return {String} 37 | */ 38 | buildCSSClass() { 39 | return `vjs-error-display ${super.buildCSSClass()}`; 40 | } 41 | 42 | /** 43 | * Generates the modal content based on the player error. 44 | * 45 | * @return {String|Null} 46 | */ 47 | content() { 48 | let error = this.player().error(); 49 | return error ? this.localize(error.message) : ''; 50 | } 51 | } 52 | 53 | ErrorDisplay.prototype.options_ = mergeOptions(ModalDialog.prototype.options_, { 54 | fillAlways: true, 55 | temporary: false, 56 | uncloseable: true 57 | }); 58 | 59 | Component.registerComponent('ErrorDisplay', ErrorDisplay); 60 | export default ErrorDisplay; 61 | -------------------------------------------------------------------------------- /src/js/utils/create-deprecation-proxy.js: -------------------------------------------------------------------------------- 1 | import log from './log.js'; 2 | 3 | /** 4 | * Object containing the default behaviors for available handler methods. 5 | * 6 | * @private 7 | * @type {Object} 8 | */ 9 | const defaultBehaviors = { 10 | get(obj, key) { 11 | return obj[key]; 12 | }, 13 | set(obj, key, value) { 14 | obj[key] = value; 15 | return true; 16 | } 17 | }; 18 | 19 | /** 20 | * Expose private objects publicly using a Proxy to log deprecation warnings. 21 | * 22 | * Browsers that do not support Proxy objects will simply return the `target` 23 | * object, so it can be directly exposed. 24 | * 25 | * @param {Object} target The target object. 26 | * @param {Object} messages Messages to display from a Proxy. Only operations 27 | * with an associated message will be proxied. 28 | * @param {String} [messages.get] 29 | * @param {String} [messages.set] 30 | * @return {Object} A Proxy if supported or the `target` argument. 31 | */ 32 | export default (target, messages={}) => { 33 | if (typeof Proxy === 'function') { 34 | let handler = {}; 35 | 36 | // Build a handler object based on those keys that have both messages 37 | // and default behaviors. 38 | Object.keys(messages).forEach(key => { 39 | if (defaultBehaviors.hasOwnProperty(key)) { 40 | handler[key] = function() { 41 | log.warn(messages[key]); 42 | return defaultBehaviors[key].apply(this, arguments); 43 | }; 44 | } 45 | }); 46 | 47 | return new Proxy(target, handler); 48 | } 49 | return target; 50 | }; 51 | -------------------------------------------------------------------------------- /src/js/popup/popup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file popup.js 3 | */ 4 | import Component from '../component.js'; 5 | import * as Dom from '../utils/dom.js'; 6 | import * as Fn from '../utils/fn.js'; 7 | import * as Events from '../utils/events.js'; 8 | 9 | /** 10 | * The Popup component is used to build pop up controls. 11 | * 12 | * @extends Component 13 | * @class Popup 14 | */ 15 | class Popup extends Component { 16 | 17 | /** 18 | * Add a popup item to the popup 19 | * 20 | * @param {Object|String} component Component or component type to add 21 | * @method addItem 22 | */ 23 | addItem(component) { 24 | this.addChild(component); 25 | component.on('click', Fn.bind(this, function(){ 26 | this.unlockShowing(); 27 | })); 28 | } 29 | 30 | /** 31 | * Create the component's DOM element 32 | * 33 | * @return {Element} 34 | * @method createEl 35 | */ 36 | createEl() { 37 | let contentElType = this.options_.contentElType || 'ul'; 38 | this.contentEl_ = Dom.createEl(contentElType, { 39 | className: 'vjs-menu-content' 40 | }); 41 | var el = super.createEl('div', { 42 | append: this.contentEl_, 43 | className: 'vjs-menu' 44 | }); 45 | el.appendChild(this.contentEl_); 46 | 47 | // Prevent clicks from bubbling up. Needed for Popup Buttons, 48 | // where a click on the parent is significant 49 | Events.on(el, 'click', function(event){ 50 | event.preventDefault(); 51 | event.stopImmediatePropagation(); 52 | }); 53 | 54 | return el; 55 | } 56 | } 57 | 58 | Component.registerComponent('Popup', Popup); 59 | export default Popup; 60 | -------------------------------------------------------------------------------- /src/js/control-bar/text-track-controls/off-text-track-menu-item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file off-text-track-menu-item.js 3 | */ 4 | import TextTrackMenuItem from './text-track-menu-item.js'; 5 | import Component from '../../component.js'; 6 | 7 | /** 8 | * A special menu item for turning of a specific type of text track 9 | * 10 | * @param {Player|Object} player 11 | * @param {Object=} options 12 | * @extends TextTrackMenuItem 13 | * @class OffTextTrackMenuItem 14 | */ 15 | class OffTextTrackMenuItem extends TextTrackMenuItem { 16 | 17 | constructor(player, options){ 18 | // Create pseudo track info 19 | // Requires options['kind'] 20 | options['track'] = { 21 | 'kind': options['kind'], 22 | 'player': player, 23 | 'label': options['kind'] + ' off', 24 | 'default': false, 25 | 'mode': 'disabled' 26 | }; 27 | 28 | // MenuItem is selectable 29 | options['selectable'] = true; 30 | 31 | super(player, options); 32 | this.selected(true); 33 | } 34 | 35 | /** 36 | * Handle text track change 37 | * 38 | * @param {Object} event Event object 39 | * @method handleTracksChange 40 | */ 41 | handleTracksChange(event){ 42 | let tracks = this.player().textTracks(); 43 | let selected = true; 44 | 45 | for (let i = 0, l = tracks.length; i < l; i++) { 46 | let track = tracks[i]; 47 | if (track['kind'] === this.track['kind'] && track['mode'] === 'showing') { 48 | selected = false; 49 | break; 50 | } 51 | } 52 | 53 | this.selected(selected); 54 | } 55 | 56 | } 57 | 58 | Component.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem); 59 | export default OffTextTrackMenuItem; 60 | -------------------------------------------------------------------------------- /src/js/control-bar/progress-control/tooltip-progress-bar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file play-progress-bar.js 3 | */ 4 | import Component from '../../component.js'; 5 | import * as Fn from '../../utils/fn.js'; 6 | import * as Dom from '../../utils/dom.js'; 7 | import formatTime from '../../utils/format-time.js'; 8 | 9 | /** 10 | * Shows play progress 11 | * 12 | * @param {Player|Object} player 13 | * @param {Object=} options 14 | * @extends Component 15 | * @class PlayProgressBar 16 | */ 17 | class TooltipProgressBar extends Component { 18 | 19 | constructor(player, options){ 20 | super(player, options); 21 | this.updateDataAttr(); 22 | this.on(player, 'timeupdate', this.updateDataAttr); 23 | player.ready(Fn.bind(this, this.updateDataAttr)); 24 | } 25 | 26 | /** 27 | * Create the component's DOM element 28 | * 29 | * @return {Element} 30 | * @method createEl 31 | */ 32 | createEl() { 33 | let el = super.createEl('div', { 34 | className: 'vjs-tooltip-progress-bar vjs-slider-bar', 35 | innerHTML: `
36 | ${this.localize('Progress')}: 0%` 37 | }); 38 | 39 | this.tooltip = el.querySelector('.vjs-time-tooltip'); 40 | 41 | return el; 42 | } 43 | 44 | updateDataAttr() { 45 | let time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime(); 46 | let formattedTime = formatTime(time, this.player_.duration()); 47 | this.el_.setAttribute('data-current-time', formattedTime); 48 | this.tooltip.innerHTML = formattedTime; 49 | } 50 | 51 | } 52 | 53 | Component.registerComponent('TooltipProgressBar', TooltipProgressBar); 54 | export default TooltipProgressBar; 55 | -------------------------------------------------------------------------------- /src/css/components/_control-bar.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-control-bar { 2 | display: none; 3 | width: 100%; 4 | position: absolute; 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | height: 3.0em; 9 | 10 | @include background-color-with-alpha($primary-background-color, $primary-background-transparency); 11 | } 12 | 13 | // Video has started playing 14 | .vjs-has-started .vjs-control-bar { 15 | @include display-flex; 16 | visibility: visible; 17 | opacity: 1; 18 | 19 | $trans: visibility 0.1s, opacity 0.1s; // Var needed because of comma 20 | @include transition($trans); 21 | } 22 | 23 | // Video has started playing AND user is inactive 24 | .vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar { 25 | visibility: hidden; 26 | opacity: 0; 27 | 28 | $trans: visibility 1.0s, opacity 1.0s; 29 | @include transition($trans); 30 | } 31 | 32 | .vjs-controls-disabled .vjs-control-bar, 33 | .vjs-using-native-controls .vjs-control-bar, 34 | .vjs-error .vjs-control-bar { 35 | // !important is ok in this context. 36 | display: none !important; 37 | } 38 | 39 | // Don't hide the control bar if it's audio 40 | .vjs-audio.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar { 41 | opacity: 1; 42 | visibility: visible; 43 | } 44 | 45 | // IE8 is flakey with fonts, and you have to change the actual content to force 46 | // fonts to show/hide properly. 47 | // - "\9" IE8 hack didn't work for this 48 | // Found in XP IE8 from http://modern.ie. Does not show up in "IE8 mode" in IE9 49 | $ie8screen: "\\0screen"; 50 | .vjs-user-inactive.vjs-playing .vjs-control-bar :before { 51 | @media #{$ie8screen} { content: ""; } 52 | } 53 | 54 | // IE 8 + 9 Support 55 | .vjs-has-started.vjs-no-flex .vjs-control-bar { 56 | display: table; 57 | } 58 | -------------------------------------------------------------------------------- /src/js/control-bar/text-track-controls/chapters-track-menu-item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file chapters-track-menu-item.js 3 | */ 4 | import MenuItem from '../../menu/menu-item.js'; 5 | import Component from '../../component.js'; 6 | import * as Fn from '../../utils/fn.js'; 7 | 8 | /** 9 | * The chapter track menu item 10 | * 11 | * @param {Player|Object} player 12 | * @param {Object=} options 13 | * @extends MenuItem 14 | * @class ChaptersTrackMenuItem 15 | */ 16 | class ChaptersTrackMenuItem extends MenuItem { 17 | 18 | constructor(player, options){ 19 | let track = options['track']; 20 | let cue = options['cue']; 21 | let currentTime = player.currentTime(); 22 | 23 | // Modify options for parent MenuItem class's init. 24 | options['label'] = cue.text; 25 | options['selected'] = (cue['startTime'] <= currentTime && currentTime < cue['endTime']); 26 | super(player, options); 27 | 28 | this.track = track; 29 | this.cue = cue; 30 | track.addEventListener('cuechange', Fn.bind(this, this.update)); 31 | } 32 | 33 | /** 34 | * Handle click on menu item 35 | * 36 | * @method handleClick 37 | */ 38 | handleClick() { 39 | super.handleClick(); 40 | this.player_.currentTime(this.cue.startTime); 41 | this.update(this.cue.startTime); 42 | } 43 | 44 | /** 45 | * Update chapter menu item 46 | * 47 | * @method update 48 | */ 49 | update() { 50 | let cue = this.cue; 51 | let currentTime = this.player_.currentTime(); 52 | 53 | // vjs.log(currentTime, cue.startTime); 54 | this.selected(cue['startTime'] <= currentTime && currentTime < cue['endTime']); 55 | } 56 | 57 | } 58 | 59 | Component.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem); 60 | export default ChaptersTrackMenuItem; 61 | -------------------------------------------------------------------------------- /test/unit/tech/tech-faker.js: -------------------------------------------------------------------------------- 1 | // Fake a media playback tech controller so that player tests 2 | // can run without HTML5 or Flash, of which PhantomJS supports neither. 3 | 4 | import Tech from '../../../src/js/tech/tech.js'; 5 | 6 | /** 7 | * @constructor 8 | */ 9 | class TechFaker extends Tech { 10 | 11 | constructor(options, handleReady){ 12 | super(options, handleReady); 13 | this.triggerReady(); 14 | } 15 | 16 | createEl() { 17 | var el = super.createEl('div', { 18 | className: 'vjs-tech' 19 | }); 20 | 21 | /*if (this.player().poster()) { 22 | // transfer the poster image to mimic HTML 23 | el.poster = this.player().poster(); 24 | }*/ 25 | 26 | return el; 27 | } 28 | 29 | // fake a poster attribute to mimic the video element 30 | poster() { return this.el().poster; } 31 | setPoster(val) { this.el().poster = val; } 32 | 33 | setControls(val) {} 34 | 35 | currentTime() { return 0; } 36 | seeking() { return false; } 37 | src() { return 'movie.mp4'; } 38 | volume() { return 0; } 39 | muted() { return false; } 40 | pause() { return false; } 41 | paused() { return true; } 42 | play() { this.trigger('play'); } 43 | supportsFullScreen() { return false; } 44 | buffered() { return {}; } 45 | duration() { return {}; } 46 | networkState() { return 0; } 47 | readyState() { return 0; } 48 | controls() { return false; } 49 | 50 | // Support everything except for "video/unsupported-format" 51 | static isSupported() { return true; } 52 | static canPlayType(type) { return (type !== 'video/unsupported-format' ? 'maybe' : ''); } 53 | static canPlaySource(srcObj) { return srcObj.type !== 'video/unsupported-format'; } 54 | } 55 | 56 | Tech.registerTech('TechFaker', TechFaker); 57 | export default TechFaker; 58 | -------------------------------------------------------------------------------- /src/js/tracks/html-track-element-list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file html-track-element-list.js 3 | */ 4 | 5 | import * as browser from '../utils/browser.js'; 6 | import document from 'global/document'; 7 | 8 | class HtmlTrackElementList { 9 | constructor(trackElements = []) { 10 | let list = this; 11 | 12 | if (browser.IS_IE8) { 13 | list = document.createElement('custom'); 14 | 15 | for (let prop in HtmlTrackElementList.prototype) { 16 | if (prop !== 'constructor') { 17 | list[prop] = HtmlTrackElementList.prototype[prop]; 18 | } 19 | } 20 | } 21 | 22 | list.trackElements_ = []; 23 | 24 | Object.defineProperty(list, 'length', { 25 | get() { 26 | return this.trackElements_.length; 27 | } 28 | }); 29 | 30 | for (let i = 0, length = trackElements.length; i < length; i++) { 31 | list.addTrackElement_(trackElements[i]); 32 | } 33 | 34 | if (browser.IS_IE8) { 35 | return list; 36 | } 37 | } 38 | 39 | addTrackElement_(trackElement) { 40 | this.trackElements_.push(trackElement); 41 | } 42 | 43 | getTrackElementByTrack_(track) { 44 | let trackElement_; 45 | 46 | for (let i = 0, length = this.trackElements_.length; i < length; i++) { 47 | if (track === this.trackElements_[i].track) { 48 | trackElement_ = this.trackElements_[i]; 49 | 50 | break; 51 | } 52 | } 53 | 54 | return trackElement_; 55 | } 56 | 57 | removeTrackElement_(trackElement) { 58 | for (let i = 0, length = this.trackElements_.length; i < length; i++) { 59 | if (trackElement === this.trackElements_[i]) { 60 | this.trackElements_.splice(i, 1); 61 | 62 | break; 63 | } 64 | } 65 | } 66 | } 67 | 68 | export default HtmlTrackElementList; 69 | -------------------------------------------------------------------------------- /test/unit/utils/create-deprecation-proxy.test.js: -------------------------------------------------------------------------------- 1 | import createDeprecationProxy from '../../../src/js/utils/create-deprecation-proxy.js'; 2 | import log from '../../../src/js/utils/log.js'; 3 | 4 | const proxySupported = typeof Proxy === 'function'; 5 | 6 | test('should return a Proxy object when supported or the target object by reference', function() { 7 | let target = {foo: 1}; 8 | let subject = createDeprecationProxy(target, { 9 | get: 'get message', 10 | set: 'set message' 11 | }); 12 | 13 | // Testing for a Proxy is really difficult because Proxy objects by their 14 | // nature disguise the fact that they are in fact Proxy objects. So, this 15 | // tests that the log.warn method gets called on property get/set operations 16 | // to detect the Proxy. 17 | if (proxySupported) { 18 | sinon.stub(log, 'warn'); 19 | 20 | subject.foo; // Triggers a "get" 21 | subject.foo = 2; // Triggers a "set" 22 | 23 | equal(log.warn.callCount, 2, 'proxied operations cause deprecation warnings'); 24 | ok(log.warn.calledWith('get message'), 'proxied get logs expected message'); 25 | ok(log.warn.calledWith('set message'), 'proxied set logs expected message'); 26 | 27 | log.warn.restore(); 28 | } else { 29 | strictEqual(target, subject, 'identical to target'); 30 | } 31 | }); 32 | 33 | // Tests run only in Proxy-supporting environments. 34 | if (proxySupported) { 35 | test('no deprecation warning is logged for operations without a message', function() { 36 | let subject = createDeprecationProxy({}, { 37 | get: 'get message' 38 | }); 39 | 40 | sinon.stub(log, 'warn'); 41 | subject.foo = 'bar'; // Triggers a "set," but not a "get" 42 | equal(log.warn.callCount, 0, 'no deprecation warning expected'); 43 | log.warn.restore(); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /lang/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Oynat", 3 | "Pause": "Duraklat", 4 | "Current Time": "Süre", 5 | "Duration Time": "Toplam Süre", 6 | "Remaining Time": "Kalan Süre", 7 | "Stream Type": "Yayın Tipi", 8 | "LIVE": "CANLI", 9 | "Loaded": "Yüklendi", 10 | "Progress": "Yükleniyor", 11 | "Fullscreen": "Tam Ekran", 12 | "Non-Fullscreen": "Küçük Ekran", 13 | "Mute": "Ses Kapa", 14 | "Unmute": "Ses Aç", 15 | "Playback Rate": "Oynatma Hızı", 16 | "Subtitles": "Altyazı", 17 | "subtitles off": "Altyazı Kapalı", 18 | "Captions": "Ek Açıklamalar", 19 | "captions off": "Ek Açıklamalar Kapalı", 20 | "Chapters": "Bölümler", 21 | "You aborted the media playback": "Video oynatmayı iptal ettiniz", 22 | "A network error caused the media download to fail part-way.": "Video indirilirken bağlantı sorunu oluştu.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video oynatılamadı, ağ ya da sunucu hatası veya belirtilen format desteklenmiyor.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Tarayıcınız desteklemediği için videoda hata oluştu.", 25 | "No compatible source was found for this media.": "Video için kaynak bulunamadı.", 26 | "Play Video": "Videoyu Oynat", 27 | "Close": "Kapat", 28 | "Modal Window": "Modal Penceresi", 29 | "This is a modal window": "Bu bir modal penceresidir", 30 | "This modal can be closed by pressing the Escape key or activating the close button.": "Bu modal ESC tuşuna basarak ya da kapata tıklanarak kapatılabilir.", 31 | ", opens captions settings dialog": ", ek açıklama ayarları menüsünü açar", 32 | ", opens subtitles settings dialog": ", altyazı ayarları menüsünü açar", 33 | ", selected": ", seçildi" 34 | } 35 | -------------------------------------------------------------------------------- /src/css/components/_captions-settings.scss: -------------------------------------------------------------------------------- 1 | .vjs-caption-settings { 2 | position: relative; 3 | top: 1em; 4 | background-color: $primary-background-color; 5 | background-color: rgba($primary-background-color, 0.75); 6 | color: $primary-foreground-color; 7 | margin: 0 auto; 8 | padding: 0.5em; 9 | height: 15em; 10 | font-size: 12px; 11 | width: 40em; 12 | } 13 | 14 | .vjs-caption-settings .vjs-tracksettings { 15 | top: 0; 16 | bottom: 2em; 17 | left: 0; 18 | right: 0; 19 | position: absolute; 20 | overflow: auto; 21 | } 22 | 23 | .vjs-caption-settings .vjs-tracksettings-colors, 24 | .vjs-caption-settings .vjs-tracksettings-font { 25 | float: left; 26 | } 27 | .vjs-caption-settings .vjs-tracksettings-colors:after, 28 | .vjs-caption-settings .vjs-tracksettings-font:after, 29 | .vjs-caption-settings .vjs-tracksettings-controls:after { 30 | clear: both; 31 | } 32 | 33 | .vjs-caption-settings .vjs-tracksettings-controls { 34 | position: absolute; 35 | bottom: 1em; 36 | right: 1em; 37 | } 38 | 39 | .vjs-caption-settings .vjs-tracksetting { 40 | margin: 5px; 41 | padding: 3px; 42 | min-height: 40px; 43 | } 44 | .vjs-caption-settings .vjs-tracksetting label { 45 | display: block; 46 | width: 100px; 47 | margin-bottom: 5px; 48 | } 49 | 50 | .vjs-caption-settings .vjs-tracksetting span { 51 | display: inline; 52 | margin-left: 5px; 53 | } 54 | 55 | .vjs-caption-settings .vjs-tracksetting > div { 56 | margin-bottom: 5px; 57 | min-height: 20px; 58 | } 59 | 60 | .vjs-caption-settings .vjs-tracksetting > div:last-child { 61 | margin-bottom: 0; 62 | padding-bottom: 0; 63 | min-height: 0; 64 | } 65 | 66 | .vjs-caption-settings label > input { 67 | margin-right: 10px; 68 | } 69 | 70 | .vjs-caption-settings input[type="button"] { 71 | width: 40px; 72 | height: 40px; 73 | } 74 | -------------------------------------------------------------------------------- /lang/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "تشغيل", 3 | "Pause": "ايقاف", 4 | "Current Time": "الوقت الحالي", 5 | "Duration Time": "Dauer", 6 | "Remaining Time": "الوقت المتبقي", 7 | "Stream Type": "نوع التيار", 8 | "LIVE": "مباشر", 9 | "Loaded": "تم التحميل", 10 | "Progress": "التقدم", 11 | "Fullscreen": "ملء الشاشة", 12 | "Non-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 | "Play Video": "تشغيل الفيديو", 27 | "Close": "أغلق", 28 | "Modal Window": "نافذة مشروطة", 29 | "This is a modal window": "هذه نافذة مشروطة", 30 | "This modal can be closed by pressing the Escape key or activating the close button.": "يمكن غلق هذه النافذة المشروطة عن طريق الضغط على زر الخروج أو تفعيل زر الإغلاق", 31 | ", opens captions settings dialog": ", تفتح نافذة خيارات التعليقات", 32 | ", opens subtitles settings dialog": ", تفتح نافذة خيارات الترجمة", 33 | ", selected": ", مختار" 34 | } 35 | -------------------------------------------------------------------------------- /src/css/components/_big-play.scss: -------------------------------------------------------------------------------- 1 | .video-js .vjs-big-play-button { 2 | font-size: 3em; 3 | line-height: $big-play-button--height; 4 | height: $big-play-button--height; 5 | width: $big-play-button--width; // Firefox bug: For some reason without width the icon wouldn't show up. Switched to using width and removed padding. 6 | display: block; 7 | position: absolute; 8 | top: 10px; 9 | left: 10px; 10 | padding: 0; 11 | cursor: pointer; 12 | opacity: 1; 13 | border: 0.06666em solid $primary-foreground-color; 14 | 15 | // Need a slightly gray bg so it can be seen on black backgrounds 16 | @include background-color-with-alpha($primary-background-color, $primary-background-transparency); 17 | @include border-radius(0.3em); 18 | @include transition(all 0.4s); 19 | 20 | @extend .vjs-icon-play; 21 | 22 | // Since the big play button doesn't inherit from vjs-control, we need to specify a bit more than 23 | // other buttons for the icon. 24 | &:before { 25 | @extend %icon-default; 26 | } 27 | } 28 | 29 | // Allow people that hate their poster image to center the big play button. 30 | .vjs-big-play-centered .vjs-big-play-button { 31 | top: 50%; 32 | left: 50%; 33 | margin-top: -($big-play-button--height / 2); 34 | margin-left: -($big-play-button--width / 2); 35 | } 36 | 37 | .video-js:hover .vjs-big-play-button, 38 | .video-js .vjs-big-play-button:focus { 39 | outline: 0; 40 | border-color: $primary-foreground-color; 41 | 42 | @include background-color-with-alpha($secondary-background-color, $secondary-background-transparency); 43 | @include transition(all 0s); 44 | } 45 | 46 | // Hide if controls are disabled, the video is playing, or native controls are used. 47 | .vjs-controls-disabled .vjs-big-play-button, 48 | .vjs-has-started .vjs-big-play-button, 49 | .vjs-using-native-controls .vjs-big-play-button, 50 | .vjs-error .vjs-big-play-button { 51 | display: none; 52 | } 53 | -------------------------------------------------------------------------------- /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 | .video-js.vjs-no-flex .vjs-menu-button-inline { 18 | // This width is currently specific to the inline volume bar. 19 | width: 12em; 20 | } 21 | // Don't transition when tabbing in reverse to the volume menu 22 | // because it looks weird 23 | .video-js .vjs-menu-button-inline.vjs-slider-active { 24 | @include transition(none); 25 | } 26 | 27 | .vjs-menu-button-inline .vjs-menu { 28 | opacity: 0; 29 | height: 100%; 30 | width: auto; 31 | 32 | position: absolute; 33 | left: 4em; 34 | top: 0; 35 | 36 | padding: 0; 37 | margin: 0; 38 | 39 | @include transition(all 0.4s); 40 | } 41 | 42 | .vjs-menu-button-inline:hover .vjs-menu, 43 | .vjs-menu-button-inline:focus .vjs-menu, 44 | .vjs-menu-button-inline.vjs-slider-active .vjs-menu { 45 | display: block; 46 | opacity: 1; 47 | } 48 | 49 | .vjs-no-flex .vjs-menu-button-inline .vjs-menu { 50 | display: block; 51 | opacity: 1; 52 | position: relative; 53 | width: auto; 54 | } 55 | 56 | .vjs-no-flex .vjs-menu-button-inline:hover .vjs-menu, 57 | .vjs-no-flex .vjs-menu-button-inline:focus .vjs-menu, 58 | .vjs-no-flex .vjs-menu-button-inline.vjs-slider-active .vjs-menu { 59 | width: auto; 60 | } 61 | 62 | .vjs-menu-button-inline .vjs-menu-content { 63 | width: auto; 64 | height: 100%; 65 | margin: 0; 66 | overflow: hidden; 67 | } 68 | -------------------------------------------------------------------------------- /sandbox/plugin.html.example: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Video.js Plugin Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 |

This page shows you how to create, register and initialize a Video.js plugin.

22 | 23 | 29 | 30 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/js/control-bar/play-toggle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file play-toggle.js 3 | */ 4 | import Button from '../button.js'; 5 | import Component from '../component.js'; 6 | 7 | /** 8 | * Button to toggle between play and pause 9 | * 10 | * @param {Player|Object} player 11 | * @param {Object=} options 12 | * @extends Button 13 | * @class PlayToggle 14 | */ 15 | class PlayToggle extends Button { 16 | 17 | constructor(player, options){ 18 | super(player, options); 19 | 20 | this.on(player, 'play', this.handlePlay); 21 | this.on(player, 'pause', this.handlePause); 22 | } 23 | 24 | /** 25 | * Allow sub components to stack CSS class names 26 | * 27 | * @return {String} The constructed class name 28 | * @method buildCSSClass 29 | */ 30 | buildCSSClass() { 31 | return `vjs-play-control ${super.buildCSSClass()}`; 32 | } 33 | 34 | /** 35 | * Handle click to toggle between play and pause 36 | * 37 | * @method handleClick 38 | */ 39 | handleClick() { 40 | if (this.player_.paused()) { 41 | this.player_.play(); 42 | } else { 43 | this.player_.pause(); 44 | } 45 | } 46 | 47 | /** 48 | * Add the vjs-playing class to the element so it can change appearance 49 | * 50 | * @method handlePlay 51 | */ 52 | handlePlay() { 53 | this.removeClass('vjs-paused'); 54 | this.addClass('vjs-playing'); 55 | this.controlText('Pause'); // change the button text to "Pause" 56 | } 57 | 58 | /** 59 | * Add the vjs-paused class to the element so it can change appearance 60 | * 61 | * @method handlePause 62 | */ 63 | handlePause() { 64 | this.removeClass('vjs-playing'); 65 | this.addClass('vjs-paused'); 66 | this.controlText('Play'); // change the button text to "Play" 67 | } 68 | 69 | } 70 | 71 | PlayToggle.prototype.controlText_ = 'Play'; 72 | 73 | Component.registerComponent('PlayToggle', PlayToggle); 74 | export default PlayToggle; 75 | -------------------------------------------------------------------------------- /src/js/control-bar/progress-control/play-progress-bar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file play-progress-bar.js 3 | */ 4 | import Component from '../../component.js'; 5 | import * as Fn from '../../utils/fn.js'; 6 | import * as Dom from '../../utils/dom.js'; 7 | import formatTime from '../../utils/format-time.js'; 8 | 9 | /** 10 | * Shows play progress 11 | * 12 | * @param {Player|Object} player 13 | * @param {Object=} options 14 | * @extends Component 15 | * @class PlayProgressBar 16 | */ 17 | class PlayProgressBar extends Component { 18 | 19 | constructor(player, options){ 20 | super(player, options); 21 | this.updateDataAttr(); 22 | this.on(player, 'timeupdate', this.updateDataAttr); 23 | player.ready(Fn.bind(this, this.updateDataAttr)); 24 | 25 | if (options.playerOptions && 26 | options.playerOptions.controlBar && 27 | options.playerOptions.controlBar.progressControl && 28 | options.playerOptions.controlBar.progressControl.keepTooltipsInside) { 29 | this.keepTooltipsInside = options.playerOptions.controlBar.progressControl.keepTooltipsInside; 30 | } 31 | 32 | if (this.keepTooltipsInside) { 33 | this.addClass('vjs-keep-tooltips-inside'); 34 | } 35 | } 36 | 37 | /** 38 | * Create the component's DOM element 39 | * 40 | * @return {Element} 41 | * @method createEl 42 | */ 43 | createEl() { 44 | return super.createEl('div', { 45 | className: 'vjs-play-progress vjs-slider-bar', 46 | innerHTML: `${this.localize('Progress')}: 0%` 47 | }); 48 | } 49 | 50 | updateDataAttr() { 51 | let time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime(); 52 | this.el_.setAttribute('data-current-time', formatTime(time, this.player_.duration())); 53 | } 54 | 55 | } 56 | 57 | Component.registerComponent('PlayProgressBar', PlayProgressBar); 58 | export default PlayProgressBar; 59 | -------------------------------------------------------------------------------- /test/unit/utils/log.test.js: -------------------------------------------------------------------------------- 1 | import log from '../../../src/js/utils/log.js'; 2 | import window from 'global/window'; 3 | 4 | q.module('log'); 5 | 6 | test('should confirm logging functions work', function() { 7 | let origConsole = window['console']; 8 | // replace the native console for testing 9 | // in ie8 console.log is apparently not a 'function' so sinon chokes on it 10 | // https://github.com/cjohansen/Sinon.JS/issues/386 11 | // instead we'll temporarily replace them with no-op functions 12 | let console = window['console'] = { 13 | log: function(){}, 14 | warn: function(){}, 15 | error: function(){} 16 | }; 17 | 18 | // stub the global log functions 19 | let logStub = sinon.stub(console, 'log'); 20 | let errorStub = sinon.stub(console, 'error'); 21 | let warnStub = sinon.stub(console, 'warn'); 22 | 23 | // Reset the log history 24 | log.history = []; 25 | 26 | log('log1', 'log2'); 27 | log.warn('warn1', 'warn2'); 28 | log.error('error1', 'error2'); 29 | 30 | ok(logStub.called, 'log was called'); 31 | equal(logStub.firstCall.args[0], 'VIDEOJS:'); 32 | equal(logStub.firstCall.args[1], 'log1'); 33 | equal(logStub.firstCall.args[2], 'log2'); 34 | 35 | ok(warnStub.called, 'warn was called'); 36 | equal(warnStub.firstCall.args[0], 'VIDEOJS:'); 37 | equal(warnStub.firstCall.args[1], 'WARN:'); 38 | equal(warnStub.firstCall.args[2], 'warn1'); 39 | equal(warnStub.firstCall.args[3], 'warn2'); 40 | 41 | ok(errorStub.called, 'error was called'); 42 | equal(errorStub.firstCall.args[0], 'VIDEOJS:'); 43 | equal(errorStub.firstCall.args[1], 'ERROR:'); 44 | equal(errorStub.firstCall.args[2], 'error1'); 45 | equal(errorStub.firstCall.args[3], 'error2'); 46 | 47 | equal(log.history.length, 3, 'there should be three messages in the log history'); 48 | 49 | // tear down sinon 50 | logStub.restore(); 51 | errorStub.restore(); 52 | warnStub.restore(); 53 | 54 | // restore the native console 55 | window['console'] = origConsole; 56 | }); 57 | -------------------------------------------------------------------------------- /src/js/tech/loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file loader.js 3 | */ 4 | import Component from '../component.js'; 5 | import Tech from './tech.js'; 6 | import window from 'global/window'; 7 | import toTitleCase from '../utils/to-title-case.js'; 8 | 9 | /** 10 | * The Media Loader is the component that decides which playback technology to load 11 | * when the player is initialized. 12 | * 13 | * @param {Object} player Main Player 14 | * @param {Object=} options Object of option names and values 15 | * @param {Function=} ready Ready callback function 16 | * @extends Component 17 | * @class MediaLoader 18 | */ 19 | class MediaLoader extends Component { 20 | 21 | constructor(player, options, ready){ 22 | super(player, options, ready); 23 | 24 | // If there are no sources when the player is initialized, 25 | // load the first supported playback technology. 26 | 27 | if (!options.playerOptions['sources'] || options.playerOptions['sources'].length === 0) { 28 | for (let i=0, j=options.playerOptions['techOrder']; i maxIndex) { 57 | throw new Error(`Failed to execute '${fnName}' on 'TimeRanges': The index provided (${index}) is greater than or equal to the maximum bound (${maxIndex}).`); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/guides/api.md: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | The Video.js API allows you to interact with the video through JavaScript, whether the browser is playing the video through HTML5 video, Flash, or any other supported playback technologies. 5 | 6 | Referencing the Player 7 | ---------------------- 8 | To use the API functions, you need access to the player object. Luckily this is easy to get. You just need to make sure your video tag has an ID. The example embed code has an ID of "example\_video_1". If you have multiple videos on one page, make sure every video tag has a unique ID. 9 | 10 | ```js 11 | var myPlayer = videojs('example_video_1'); 12 | ``` 13 | 14 | (If the player hasn't been initialized yet via the data-setup attribute or another method, this will also initialize the player.) 15 | 16 | Wait Until the Player is Ready 17 | ------------------------------ 18 | The time it takes Video.js to set up the video and API will vary depending on the playback technology being used (HTML5 will often be much faster to load than Flash). For that reason we want to use the player's 'ready' function to trigger any code that requires the player's API. 19 | 20 | ```js 21 | videojs("example_video_1").ready(function(){ 22 | var myPlayer = this; 23 | 24 | // EXAMPLE: Start playing the video. 25 | myPlayer.play(); 26 | 27 | }); 28 | ``` 29 | 30 | API Methods 31 | ----------- 32 | Now that you have access to a ready player, you can control the video, get values, or respond to video events. The Video.js API function names follow the [HTML5 media API](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html). The main difference is that getter/setter functions are used for video properties. 33 | 34 | ```js 35 | 36 | // setting a property on a bare HTML5 video element 37 | myVideoElement.currentTime = "120"; 38 | 39 | // setting a property on a Video.js player 40 | myPlayer.currentTime(120); 41 | 42 | ``` 43 | 44 | The full list of player API methods and events can be found in the [player API docs](http://docs.videojs.com/docs/api/index.html). 45 | -------------------------------------------------------------------------------- /lang/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Odtwarzaj", 3 | "Pause": "Pauza", 4 | "Current Time": "Aktualny czas", 5 | "Duration Time": "Czas trwania", 6 | "Remaining Time": "Pozostały czas", 7 | "Stream Type": "Typ strumienia", 8 | "LIVE": "NA ŻYWO", 9 | "Loaded": "Załadowany", 10 | "Progress": "Status", 11 | "Fullscreen": "Pełny ekran", 12 | "Non-Fullscreen": "Pełny ekran niedostępny", 13 | "Mute": "Wyłącz dźwięk", 14 | "Unmute": "Włącz dźwięk", 15 | "Playback Rate": "Szybkość odtwarzania", 16 | "Subtitles": "Napisy", 17 | "subtitles off": "Napisy wyłączone", 18 | "Captions": "Transkrypcja", 19 | "captions off": "Transkrypcja wyłączona", 20 | "Chapters": "Rozdziały", 21 | "You aborted the media playback": "Odtwarzanie zostało przerwane", 22 | "A network error caused the media download to fail part-way.": "Problemy z siecią spowodowały błąd przy pobieraniu materiału wideo.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Materiał wideo nie może być załadowany, ponieważ wystąpił problem z siecią lub format nie jest obsługiwany", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Odtwarzanie materiału wideo zostało przerwane z powodu uszkodzonego pliku wideo lub z powodu błędu funkcji, które nie są wspierane przez przeglądarkę.", 25 | "No compatible source was found for this media.": "Dla tego materiału wideo nie znaleziono kompatybilnego źródła.", 26 | "Play video": "Odtwarzaj wideo", 27 | "Close": "Zamknij", 28 | "Modal Window": "Okno Modala", 29 | "This is a modal window": "To jest okno modala", 30 | "This modal can be closed by pressing the Escape key or activating the close button.": "Ten modal możesz zamknąć naciskając przycisk Escape albo wybierając przycisk Zamknij.", 31 | ", opens captions settings dialog": ", otwiera okno dialogowe ustawień transkrypcji", 32 | ", opens subtitles settings dialog": ", otwiera okno dialogowe napisów", 33 | ", selected": ", zaznaczone" 34 | } 35 | -------------------------------------------------------------------------------- /src/js/utils/merge-options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file merge-options.js 3 | */ 4 | import merge from 'lodash-compat/object/merge'; 5 | 6 | function isPlain(obj) { 7 | return !!obj 8 | && typeof obj === 'object' 9 | && obj.toString() === '[object Object]' 10 | && obj.constructor === Object; 11 | } 12 | 13 | /** 14 | * Merge customizer. video.js simply overwrites non-simple objects 15 | * (like arrays) instead of attempting to overlay them. 16 | * @see https://lodash.com/docs#merge 17 | */ 18 | const customizer = function(destination, source) { 19 | // If we're not working with a plain object, copy the value as is 20 | // If source is an array, for instance, it will replace destination 21 | if (!isPlain(source)) { 22 | return source; 23 | } 24 | 25 | // If the new value is a plain object but the first object value is not 26 | // we need to create a new object for the first object to merge with. 27 | // This makes it consistent with how merge() works by default 28 | // and also protects from later changes the to first object affecting 29 | // the second object's values. 30 | if (!isPlain(destination)) { 31 | return mergeOptions(source); 32 | } 33 | }; 34 | 35 | /** 36 | * Merge one or more options objects, recursively merging **only** 37 | * plain object properties. Previously `deepMerge`. 38 | * 39 | * @param {...Object} source One or more objects to merge 40 | * @returns {Object} a new object that is the union of all 41 | * provided objects 42 | * @function mergeOptions 43 | */ 44 | export default function mergeOptions() { 45 | // contruct the call dynamically to handle the variable number of 46 | // objects to merge 47 | let args = Array.prototype.slice.call(arguments); 48 | 49 | // unshift an empty object into the front of the call as the target 50 | // of the merge 51 | args.unshift({}); 52 | 53 | // customize conflict resolution to match our historical merge behavior 54 | args.push(customizer); 55 | 56 | merge.apply(null, args); 57 | 58 | // return the mutated result object 59 | return args[0]; 60 | } 61 | -------------------------------------------------------------------------------- /src/js/utils/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file log.js 3 | */ 4 | import window from 'global/window'; 5 | 6 | /** 7 | * Log plain debug messages 8 | */ 9 | const log = function(){ 10 | _logType(null, arguments); 11 | }; 12 | 13 | /** 14 | * Keep a history of log messages 15 | * @type {Array} 16 | */ 17 | log.history = []; 18 | 19 | /** 20 | * Log error messages 21 | */ 22 | log.error = function(){ 23 | _logType('error', arguments); 24 | }; 25 | 26 | /** 27 | * Log warning messages 28 | */ 29 | log.warn = function(){ 30 | _logType('warn', arguments); 31 | }; 32 | 33 | /** 34 | * Log messages to the console and history based on the type of message 35 | * 36 | * @param {String} type The type of message, or `null` for `log` 37 | * @param {Object} args The args to be passed to the log 38 | * @private 39 | * @method _logType 40 | */ 41 | function _logType(type, args){ 42 | // convert args to an array to get array functions 43 | let argsArray = Array.prototype.slice.call(args); 44 | // if there's no console then don't try to output messages 45 | // they will still be stored in log.history 46 | // Was setting these once outside of this function, but containing them 47 | // in the function makes it easier to test cases where console doesn't exist 48 | let noop = function(){}; 49 | 50 | let console = window['console'] || { 51 | 'log': noop, 52 | 'warn': noop, 53 | 'error': noop 54 | }; 55 | 56 | if (type) { 57 | // add the type to the front of the message 58 | argsArray.unshift(type.toUpperCase()+':'); 59 | } else { 60 | // default to log with no prefix 61 | type = 'log'; 62 | } 63 | 64 | // add to history 65 | log.history.push(argsArray); 66 | 67 | // add console prefix after adding to history 68 | argsArray.unshift('VIDEOJS:'); 69 | 70 | // call appropriate log function 71 | if (console[type].apply) { 72 | console[type].apply(console, argsArray); 73 | } else { 74 | // ie8 doesn't allow error.apply, but it will just join() the array anyway 75 | console[type](argsArray.join(' ')); 76 | } 77 | } 78 | 79 | export default log; 80 | -------------------------------------------------------------------------------- /lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Play", 3 | "Pause": "Pause", 4 | "Current Time": "Current Time", 5 | "Duration Time": "Duration Time", 6 | "Remaining Time": "Remaining Time", 7 | "Stream Type": "Stream Type", 8 | "LIVE": "LIVE", 9 | "Loaded": "Loaded", 10 | "Progress": "Progress", 11 | "Fullscreen": "Fullscreen", 12 | "Non-Fullscreen": "Non-Fullscreen", 13 | "Mute": "Mute", 14 | "Unmute": "Unmute", 15 | "Playback Rate": "Playback Rate", 16 | "Subtitles": "Subtitles", 17 | "subtitles off": "subtitles off", 18 | "Captions": "Captions", 19 | "captions off": "captions off", 20 | "Chapters": "Chapters", 21 | "Descriptions": "Descriptions", 22 | "descriptions off": "descriptions off", 23 | "You aborted the media playback": "You aborted the media playback", 24 | "A network error caused the media download to fail part-way.": "A network error caused the media download to fail part-way.", 25 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "The media could not be loaded, either because the server or network failed or because the format is not supported.", 26 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.", 27 | "No compatible source was found for this media.": "No compatible source was found for this media.", 28 | "Play Video": "Play Video", 29 | "Close": "Close", 30 | "Modal Window": "Modal Window", 31 | "This is a modal window": "This is a modal window", 32 | "This modal can be closed by pressing the Escape key or activating the close button.": "This modal can be closed by pressing the Escape key or activating the close button.", 33 | ", opens captions settings dialog": ", opens captions settings dialog", 34 | ", opens subtitles settings dialog": ", opens subtitles settings dialog", 35 | ", opens descriptions settings dialog": ", opens descriptions settings dialog", 36 | ", selected": ", selected" 37 | } 38 | -------------------------------------------------------------------------------- /lang/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Wiedergabe", 3 | "Pause": "Pause", 4 | "Current Time": "Aktueller Zeitpunkt", 5 | "Duration Time": "Dauer", 6 | "Remaining Time": "Verbleibende Zeit", 7 | "Stream Type": "Streamtyp", 8 | "LIVE": "LIVE", 9 | "Loaded": "Geladen", 10 | "Progress": "Status", 11 | "Fullscreen": "Vollbild", 12 | "Non-Fullscreen": "Kein Vollbild", 13 | "Mute": "Ton aus", 14 | "Unmute": "Ton ein", 15 | "Playback Rate": "Wiedergabegeschwindigkeit", 16 | "Subtitles": "Untertitel", 17 | "subtitles off": "Untertitel aus", 18 | "Captions": "Untertitel", 19 | "captions off": "Untertitel aus", 20 | "Chapters": "Kapitel", 21 | "You aborted the media playback": "Sie haben die Videowiedergabe abgebrochen.", 22 | "A network error caused the media download to fail part-way.": "Der Videodownload ist aufgrund eines Netzwerkfehlers fehlgeschlagen.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Das Video konnte nicht geladen werden, da entweder ein Server- oder Netzwerkfehler auftrat oder das Format nicht unterstützt wird.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Die Videowiedergabe wurde entweder wegen eines Problems mit einem beschädigten Video oder wegen verwendeten Funktionen, die vom Browser nicht unterstützt werden, abgebrochen.", 25 | "No compatible source was found for this media.": "Für dieses Video wurde keine kompatible Quelle gefunden.", 26 | "Play Video": "Video abspielen", 27 | "Close": "Schließen", 28 | "Modal Window": "Modales Fenster", 29 | "This is a modal window": "Dies ist ein modales Fenster", 30 | "This modal can be closed by pressing the Escape key or activating the close button.": "Durch Drücken der Esc-Taste bzw. Betätigung der Schaltfläche \"Schließen\" wird dieses modale Fenster geschlossen.", 31 | ", opens captions settings dialog": ", öffnet Einstellungen für Untertitel", 32 | ", opens subtitles settings dialog": ", öffnet Einstellungen für Untertitel", 33 | ", selected": " (ausgewählt)" 34 | } 35 | -------------------------------------------------------------------------------- /src/js/fullscreen-api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file fullscreen-api.js 3 | */ 4 | import document from 'global/document'; 5 | 6 | /* 7 | * Store the browser-specific methods for the fullscreen API 8 | * @type {Object|undefined} 9 | * @private 10 | */ 11 | let FullscreenApi = {}; 12 | 13 | // browser API methods 14 | // map approach from Screenful.js - https://github.com/sindresorhus/screenfull.js 15 | const apiMap = [ 16 | // Spec: https://dvcs.w3.org/hg/fullscreen/raw-file/tip/Overview.html 17 | [ 18 | 'requestFullscreen', 19 | 'exitFullscreen', 20 | 'fullscreenElement', 21 | 'fullscreenEnabled', 22 | 'fullscreenchange', 23 | 'fullscreenerror' 24 | ], 25 | // WebKit 26 | [ 27 | 'webkitRequestFullscreen', 28 | 'webkitExitFullscreen', 29 | 'webkitFullscreenElement', 30 | 'webkitFullscreenEnabled', 31 | 'webkitfullscreenchange', 32 | 'webkitfullscreenerror' 33 | ], 34 | // Old WebKit (Safari 5.1) 35 | [ 36 | 'webkitRequestFullScreen', 37 | 'webkitCancelFullScreen', 38 | 'webkitCurrentFullScreenElement', 39 | 'webkitCancelFullScreen', 40 | 'webkitfullscreenchange', 41 | 'webkitfullscreenerror' 42 | ], 43 | // Mozilla 44 | [ 45 | 'mozRequestFullScreen', 46 | 'mozCancelFullScreen', 47 | 'mozFullScreenElement', 48 | 'mozFullScreenEnabled', 49 | 'mozfullscreenchange', 50 | 'mozfullscreenerror' 51 | ], 52 | // Microsoft 53 | [ 54 | 'msRequestFullscreen', 55 | 'msExitFullscreen', 56 | 'msFullscreenElement', 57 | 'msFullscreenEnabled', 58 | 'MSFullscreenChange', 59 | 'MSFullscreenError' 60 | ] 61 | ]; 62 | 63 | let specApi = apiMap[0]; 64 | let browserApi; 65 | 66 | // determine the supported set of functions 67 | for (let i = 0; i < apiMap.length; i++) { 68 | // check for exitFullscreen function 69 | if (apiMap[i][1] in document) { 70 | browserApi = apiMap[i]; 71 | break; 72 | } 73 | } 74 | 75 | // map the browser API names to the spec API names 76 | if (browserApi) { 77 | for (let i=0; iCurrent Time ' + '0:00', 39 | }, { 40 | // tell screen readers not to automatically read the time as it changes 41 | 'aria-live': 'off' 42 | }); 43 | 44 | el.appendChild(this.contentEl_); 45 | return el; 46 | } 47 | 48 | /** 49 | * Update current time display 50 | * 51 | * @method updateContent 52 | */ 53 | updateContent() { 54 | // Allows for smooth scrubbing, when player can't keep up. 55 | let time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime(); 56 | let localizedText = this.localize('Current Time'); 57 | let formattedTime = formatTime(time, this.player_.duration()); 58 | if (formattedTime !== this.formattedTime_) { 59 | this.formattedTime_ = formattedTime; 60 | this.contentEl_.innerHTML = `${localizedText} ${formattedTime}`; 61 | } 62 | } 63 | 64 | } 65 | 66 | Component.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay); 67 | export default CurrentTimeDisplay; 68 | -------------------------------------------------------------------------------- /sandbox/index.html.example: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Video.js Sandbox 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 |
22 |

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 make a copy of index.html.example and rename it to index.html.

23 |
cp sandbox/index.html.example sandbox/index.html
24 |
grunt watch
25 |
grunt connect
26 |
open http://localhost:9999/sandbox/index.html
27 |
28 | 29 | 38 | 39 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /sandbox/icons.html.example: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Video.js Icons Sandbox 6 | 7 | 8 | 9 | 12 | 13 | 14 |

Video.js Icons

15 |

This is a list of all of the icons available in the Video.js base stylesheet. The appropriate class is to the right of each icon.

16 | 17 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /lang/el.json: -------------------------------------------------------------------------------- 1 | { 2 | "Play": "Aναπαραγωγή", 3 | "Pause": "Παύση", 4 | "Current Time": "Τρέχων χρόνος", 5 | "Duration Time": "Συνολικός χρόνος", 6 | "Remaining Time": "Υπολοιπόμενος χρόνος", 7 | "Stream Type": "Τύπος ροής", 8 | "LIVE": "ΖΩΝΤΑΝΑ", 9 | "Loaded": "Φόρτωση επιτυχής", 10 | "Progress": "Πρόοδος", 11 | "Fullscreen": "Πλήρης οθόνη", 12 | "Non-Fullscreen": "Έξοδος από πλήρη οθόνη", 13 | "Mute": "Σίγαση", 14 | "Unmute": "Kατάργηση σίγασης", 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 | "Play video": "Αναπαραγωγή βίντεο", 27 | "Close": "Κλείσιμο", 28 | "Modal Window": "Aναδυόμενο παράθυρο", 29 | "This is a modal window": "Το παρών είναι ένα αναδυόμενο παράθυρο", 30 | "This modal can be closed by pressing the Escape key or activating the close button.": "Αυτό το παράθυρο μπορεί να εξαφανιστεί πατώντας το πλήκτρο Escape ή πατώντας το κουμπί κλεισίματος.", 31 | ", opens captions settings dialog": ", εμφανίζει τις ρυθμίσεις για τις λεζάντες", 32 | ", opens subtitles settings dialog": ", εμφανίζει τις ρυθμίσεις για τους υπότιτλους", 33 | ", selected": ", επιλεγμένο" 34 | } 35 | -------------------------------------------------------------------------------- /src/js/control-bar/text-track-controls/text-track-button.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file text-track-button.js 3 | */ 4 | import MenuButton from '../../menu/menu-button.js'; 5 | import Component from '../../component.js'; 6 | import * as Fn from '../../utils/fn.js'; 7 | import TextTrackMenuItem from './text-track-menu-item.js'; 8 | import OffTextTrackMenuItem from './off-text-track-menu-item.js'; 9 | 10 | /** 11 | * The base class for buttons that toggle specific text track types (e.g. subtitles) 12 | * 13 | * @param {Player|Object} player 14 | * @param {Object=} options 15 | * @extends MenuButton 16 | * @class TextTrackButton 17 | */ 18 | class TextTrackButton extends MenuButton { 19 | 20 | constructor(player, options){ 21 | super(player, options); 22 | 23 | let tracks = this.player_.textTracks(); 24 | 25 | if (this.items.length <= 1) { 26 | this.hide(); 27 | } 28 | 29 | if (!tracks) { 30 | return; 31 | } 32 | 33 | let updateHandler = Fn.bind(this, this.update); 34 | tracks.addEventListener('removetrack', updateHandler); 35 | tracks.addEventListener('addtrack', updateHandler); 36 | 37 | this.player_.on('dispose', function() { 38 | tracks.removeEventListener('removetrack', updateHandler); 39 | tracks.removeEventListener('addtrack', updateHandler); 40 | }); 41 | } 42 | 43 | // Create a menu item for each text track 44 | createItems(items=[]) { 45 | // Add an OFF menu item to turn all tracks off 46 | items.push(new OffTextTrackMenuItem(this.player_, { 'kind': this.kind_ })); 47 | 48 | let tracks = this.player_.textTracks(); 49 | 50 | if (!tracks) { 51 | return items; 52 | } 53 | 54 | for (let i = 0; i < tracks.length; i++) { 55 | let track = tracks[i]; 56 | 57 | // only add tracks that are of the appropriate kind and have a label 58 | if (track['kind'] === this.kind_) { 59 | items.push(new TextTrackMenuItem(this.player_, { 60 | // MenuItem is selectable 61 | 'selectable': true, 62 | 'track': track 63 | })); 64 | } 65 | } 66 | 67 | return items; 68 | } 69 | 70 | } 71 | 72 | Component.registerComponent('TextTrackButton', TextTrackButton); 73 | export default TextTrackButton; 74 | -------------------------------------------------------------------------------- /src/js/control-bar/text-track-controls/captions-button.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file captions-button.js 3 | */ 4 | import TextTrackButton from './text-track-button.js'; 5 | import Component from '../../component.js'; 6 | import CaptionSettingsMenuItem from './caption-settings-menu-item.js'; 7 | 8 | /** 9 | * The button component for toggling and selecting captions 10 | * 11 | * @param {Object} player Player object 12 | * @param {Object=} options Object of option names and values 13 | * @param {Function=} ready Ready callback function 14 | * @extends TextTrackButton 15 | * @class CaptionsButton 16 | */ 17 | class CaptionsButton extends TextTrackButton { 18 | 19 | constructor(player, options, ready){ 20 | super(player, options, ready); 21 | this.el_.setAttribute('aria-label','Captions Menu'); 22 | } 23 | 24 | /** 25 | * Allow sub components to stack CSS class names 26 | * 27 | * @return {String} The constructed class name 28 | * @method buildCSSClass 29 | */ 30 | buildCSSClass() { 31 | return `vjs-captions-button ${super.buildCSSClass()}`; 32 | } 33 | 34 | /** 35 | * Update caption menu items 36 | * 37 | * @method update 38 | */ 39 | update() { 40 | let threshold = 2; 41 | super.update(); 42 | 43 | // if native, then threshold is 1 because no settings button 44 | if (this.player().tech_ && this.player().tech_['featuresNativeTextTracks']) { 45 | threshold = 1; 46 | } 47 | 48 | if (this.items && this.items.length > threshold) { 49 | this.show(); 50 | } else { 51 | this.hide(); 52 | } 53 | } 54 | 55 | /** 56 | * Create caption menu items 57 | * 58 | * @return {Array} Array of menu items 59 | * @method createItems 60 | */ 61 | createItems() { 62 | let items = []; 63 | 64 | if (!(this.player().tech_ && this.player().tech_['featuresNativeTextTracks'])) { 65 | items.push(new CaptionSettingsMenuItem(this.player_, { 'kind': this.kind_ })); 66 | } 67 | 68 | return super.createItems(items); 69 | } 70 | 71 | } 72 | 73 | CaptionsButton.prototype.kind_ = 'captions'; 74 | CaptionsButton.prototype.controlText_ = 'Captions'; 75 | 76 | Component.registerComponent('CaptionsButton', CaptionsButton); 77 | export default CaptionsButton; 78 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | [Video.js homepage](http://videojs.com) 2 | 3 |

Video.js Documentation

4 | 5 | There are two categories of docs: [Guides](./guides/) and [API docs](./api/). Guides explain general topics and use cases (e.g. setup). API docs are automatically generated from the codebase and give specific details about functions, properties, and events. 6 | 7 | (Corrections and additions welcome) 8 | 9 | ## Guides 10 | 11 | ### Getting Started 12 | 13 | * [Setup](./guides/setup.md) - The setup documentation gives a deeper view of the additional methods you can use to trigger the player setup. 14 | 15 | * [Options](./guides/options.md) - There are a number of options that can be used to change how the player behaves, starting with the HTML5 media options like autoplay and preload, and expanding to Video.js specific options. 16 | 17 | * [Tracks](./guides/tracks.md) - Text Tracks are used to display subtitles and captions, and add a menu for navigating between chapters in a video. 18 | 19 | ### Customizing 20 | 21 | * [API](./guides/api.md) - The Video.js API allows you to control the video through javascript or trigger event listeners, whether the video is playing through HTML5, flash, or another playback technology. 22 | 23 | * [Skins](./guides/skins.md) - You can change the look of the player across playback technologies just by editing a CSS file. The skins documentation gives you a intro to how the HTML and CSS of the default skin is put together. 24 | 25 | * [Tech](./guides/tech.md) - A 'playback technology' is the term we're using to represent HTML5 video, Flash, and other video plugins, as well as other players like the YouTube player. Basically anything that has a unique API to audio or video. Additional playback technologies can be added relatively easily. 26 | 27 | * [Plugins](./guides/plugins.md) - You can package up interesting Video.js customizations and reuse them elsewhere. Find out how to build your own plugin or use one created by someone else. 28 | 29 | ### Resources 30 | 31 | * [Glossary](./guides/glossary.md) - Some helpful definitions. 32 | 33 | * [Removing Players](./guides/removing-players.md) - Helpful for using VideoJS in single page apps. 34 | 35 | ## API Docs 36 | - The most relevant API doc is the [player API doc](./api/vjs.Player.md). 37 | - [Full list of API Docs](./api/) 38 | -------------------------------------------------------------------------------- /test/unit/tech/flash-rtmp.test.js: -------------------------------------------------------------------------------- 1 | import Flash from '../../../src/js/tech/flash.js'; 2 | 3 | q.module('Flash RTMP'); 4 | 5 | var streamToPartsAndBack = function(url) { 6 | var parts = Flash.streamToParts(url); 7 | return Flash.streamFromParts(parts.connection, parts.stream); 8 | }; 9 | 10 | test('test using both streamToParts and streamFromParts', function() { 11 | ok('rtmp://myurl.com/&isthis' === streamToPartsAndBack('rtmp://myurl.com/isthis')); 12 | ok('rtmp://myurl.com/&isthis' === streamToPartsAndBack('rtmp://myurl.com/&isthis')); 13 | ok('rtmp://myurl.com/isthis/&andthis' === streamToPartsAndBack('rtmp://myurl.com/isthis/andthis')); 14 | }); 15 | 16 | test('test streamToParts', function() { 17 | var parts = Flash.streamToParts('http://myurl.com/streaming&/is/fun'); 18 | ok(parts.connection === 'http://myurl.com/streaming'); 19 | ok(parts.stream === '/is/fun'); 20 | 21 | parts = Flash.streamToParts('http://myurl.com/&streaming&/is/fun'); 22 | ok(parts.connection === 'http://myurl.com/'); 23 | ok(parts.stream === 'streaming&/is/fun'); 24 | 25 | parts = Flash.streamToParts('http://myurl.com/really?streaming=fun&really=fun'); 26 | ok(parts.connection === 'http://myurl.com/'); 27 | ok(parts.stream === 'really?streaming=fun&really=fun'); 28 | 29 | parts = Flash.streamToParts('http://myurl.com/streaming/is/fun'); 30 | ok(parts.connection === 'http://myurl.com/streaming/is/'); 31 | ok(parts.stream === 'fun'); 32 | 33 | parts = Flash.streamToParts('whatisgoingonhere'); 34 | ok(parts.connection === 'whatisgoingonhere'); 35 | ok(parts.stream === ''); 36 | 37 | parts = Flash.streamToParts(); 38 | ok(parts.connection === ''); 39 | ok(parts.stream === ''); 40 | }); 41 | 42 | test('test isStreamingSrc', function() { 43 | var isStreamingSrc = Flash.isStreamingSrc; 44 | ok(isStreamingSrc('rtmp://streaming.is/fun')); 45 | ok(isStreamingSrc('rtmps://streaming.is/fun')); 46 | ok(isStreamingSrc('rtmpe://streaming.is/fun')); 47 | ok(isStreamingSrc('rtmpt://streaming.is/fun')); 48 | // test invalid protocols 49 | ok(!isStreamingSrc('rtmp:streaming.is/fun')); 50 | ok(!isStreamingSrc('rtmpz://streaming.is/fun')); 51 | ok(!isStreamingSrc('http://streaming.is/fun')); 52 | ok(!isStreamingSrc('https://streaming.is/fun')); 53 | ok(!isStreamingSrc('file://streaming.is/fun')); 54 | }); 55 | -------------------------------------------------------------------------------- /src/js/popup/popup-button.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file popup-button.js 3 | */ 4 | import ClickableComponent from '../clickable-component.js'; 5 | import Component from '../component.js'; 6 | import Popup from './popup.js'; 7 | import * as Dom from '../utils/dom.js'; 8 | import * as Fn from '../utils/fn.js'; 9 | import toTitleCase from '../utils/to-title-case.js'; 10 | 11 | /** 12 | * A button class with a popup control 13 | * 14 | * @param {Player|Object} player 15 | * @param {Object=} options 16 | * @extends ClickableComponent 17 | * @class PopupButton 18 | */ 19 | class PopupButton extends ClickableComponent { 20 | 21 | constructor(player, options={}){ 22 | super(player, options); 23 | 24 | this.update(); 25 | } 26 | 27 | /** 28 | * Update popup 29 | * 30 | * @method update 31 | */ 32 | update() { 33 | let popup = this.createPopup(); 34 | 35 | if (this.popup) { 36 | this.removeChild(this.popup); 37 | } 38 | 39 | this.popup = popup; 40 | this.addChild(popup); 41 | 42 | if (this.items && this.items.length === 0) { 43 | this.hide(); 44 | } else if (this.items && this.items.length > 1) { 45 | this.show(); 46 | } 47 | } 48 | 49 | /** 50 | * Create popup - Override with specific functionality for component 51 | * 52 | * @return {Popup} The constructed popup 53 | * @method createPopup 54 | */ 55 | createPopup() {} 56 | 57 | /** 58 | * Create the component's DOM element 59 | * 60 | * @return {Element} 61 | * @method createEl 62 | */ 63 | createEl() { 64 | return super.createEl('div', { 65 | className: this.buildCSSClass() 66 | }); 67 | } 68 | 69 | /** 70 | * Allow sub components to stack CSS class names 71 | * 72 | * @return {String} The constructed class name 73 | * @method buildCSSClass 74 | */ 75 | buildCSSClass() { 76 | var menuButtonClass = 'vjs-menu-button'; 77 | 78 | // If the inline option is passed, we want to use different styles altogether. 79 | if (this.options_.inline === true) { 80 | menuButtonClass += '-inline'; 81 | } else { 82 | menuButtonClass += '-popup'; 83 | } 84 | 85 | return `vjs-menu-button ${menuButtonClass} ${super.buildCSSClass()}`; 86 | } 87 | 88 | } 89 | 90 | Component.registerComponent('PopupButton', PopupButton); 91 | export default PopupButton; 92 | -------------------------------------------------------------------------------- /test/unit/menu.test.js: -------------------------------------------------------------------------------- 1 | import MenuButton from '../../src/js/menu/menu-button.js'; 2 | import TestHelpers from './test-helpers.js'; 3 | import * as Events from '../../src/js/utils/events.js'; 4 | 5 | q.module('MenuButton'); 6 | 7 | q.test('should not throw an error when there is no children', function() { 8 | expect(0); 9 | let player = TestHelpers.makePlayer(); 10 | 11 | let menuButton = new MenuButton(player); 12 | let el = menuButton.el(); 13 | 14 | try { 15 | Events.trigger(el, 'click'); 16 | } catch (error) { 17 | ok(!error, 'click should not throw anything'); 18 | } 19 | 20 | player.dispose(); 21 | }); 22 | 23 | q.test('should place title list item into ul', function() { 24 | var player, menuButton; 25 | player = TestHelpers.makePlayer(); 26 | 27 | menuButton = new MenuButton(player, { 28 | 'title': 'testTitle' 29 | }); 30 | 31 | let menuContentElement = menuButton.el().getElementsByTagName('UL')[0]; 32 | let titleElement = menuContentElement.children[0]; 33 | 34 | ok(titleElement.innerHTML === 'TestTitle', 'title element placed in ul'); 35 | 36 | player.dispose(); 37 | }); 38 | 39 | q.test('clicking should display the menu', function() { 40 | expect(6); 41 | 42 | let player = TestHelpers.makePlayer(); 43 | 44 | // Make sure there's some content in the menu, even if it's just a title! 45 | let menuButton = new MenuButton(player, { 46 | 'title': 'testTitle' 47 | }); 48 | let el = menuButton.el(); 49 | 50 | ok(menuButton.menu !== undefined, 'menu is created'); 51 | 52 | equal(menuButton.menu.hasClass('vjs-lock-showing'), false, 'menu defaults to hidden'); 53 | 54 | Events.trigger(el, 'click'); 55 | 56 | equal(menuButton.menu.hasClass('vjs-lock-showing'), true, 'clicking on the menu button shows the menu'); 57 | 58 | Events.trigger(el, 'click'); 59 | 60 | equal(menuButton.menu.hasClass('vjs-lock-showing'), false, 'clicking again on the menu button hides the menu'); 61 | 62 | menuButton.disable(); 63 | 64 | Events.trigger(el, 'click'); 65 | 66 | equal(menuButton.menu.hasClass('vjs-lock-showing'), false, 'disable() prevents clicking from showing the menu'); 67 | 68 | menuButton.enable(); 69 | 70 | Events.trigger(el, 'click'); 71 | 72 | equal(menuButton.menu.hasClass('vjs-lock-showing'), true, 'enable() allows clicking to show the menu'); 73 | 74 | player.dispose(); 75 | }); 76 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.12 4 | before_install: 5 | - export CHROME_BIN=/usr/bin/google-chrome 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | notifications: 9 | irc: 10 | channels: 11 | - chat.freenode.net#videojs 12 | use_notice: true 13 | on_success: never 14 | webhooks: 15 | - http://pam.videojs.com/savage/travis 16 | env: 17 | global: 18 | - secure: K6JpKwMkfNaJix3Bb0tLjVMzHMJgtBXdd/dvfw1BMb9DCBpd81PqXbDs7yXCddUxnUPTBPxZCrQgWsw71Wn+qEoIG5MU3uOT5A2rBbx/yZonVAGv5ed/9w0xk0OzO383CmPMFqwqtp9YmdmqGjQBkYXVXJjTvNTOAExFSdhO+3U= 19 | - secure: GIbhjUJapvC70nIZVlhVyK+3KAD2TVKpiY/q412OO7V2izbvcM1tvU3LBoMZbROzrt5TT84tCoJDvHnrpL0OvxPwrzL5CUU7h4UTxhTOyQkEinbYAnWlW9wdrvtdczsEvANkFPqBZ53B3hVHZHMLOG8QRWaTBicF68vSHEJFqb4= 20 | - secure: gglh7xDnURKfXp9T543DD7NG1pQ8HeWh1XtRspBAwr0H7RqJBVDqqODSYSPRFhfld7M6sYmvQIXgil7XlyefnKNTXqCarvaoTg3lbip8kSltXMiNw2V6AVpsQGuja7+XbaM0do70ETTKjW4Kw6wnxEHb78BvGN/hXIeqizUAjanlDAjd7fouaxpTBIbMESe2rI+WRHPis1cmnv8v70Mrh/8Un/NO4gkebGyvA47LTDNIaVqIVjonsndr8WjMv1/PNxQ8LyCO6D64MufrobS7Sec+VuN30apwEsBw8v82MK/MZ3qXu0lUp4+ERTbuc/rymh2wDFTQeG20Kf/NTauSaH6f414KNzIRFj0/xyLAzVZKIscXM2DKXMuskkZuvHLZvaspnZWcPYTjPZl0P88N0RBqnoLdR80dR5bDljNwU2QnSBeol/q1wXNEr6I1VTRFOB+qsHrD1blVMB1I5W3I0ti1aQ7XtgMOGi1kcPb4oFcJdl+3dLFDnyRyaNfdMOnOZYBBHdQCo19Mj/L+nqPGWeeYiEAM6JsuhNjHn5Za5nGf1ztXTimVPOQjyATin0x9kST3soLWSVmdW2dBHUGDVSMhvoLLR+nKSdNQ0KfpqtgrzeLxoVnRYHVBlih41tapM9IG/6BMYnDMaRcc0i54YeUP4oxlxGSyASIenkAgC6w= 21 | - secure: WtIEOSnqDkCZuTlBsxwlVwaRpVTbz7ol8+XSJIZb0aFo1lLisF9cz6s9WrAfX36MaxIcDN9LFZkpXzMvNrNkZWQa1kacGWH1rbx0SiiQ8LMweAcKdnZ5uXlSplBxbJ8bZfXKB1sIHsOsYw/vWhHKkcsDUkAEzQrIiMOhuoUV3s0uKM0knKXIAfNIF0EbDzLIojm+nm+F0n5vM60LRdKesaSt/o2p2LKxdZVoFGrg48D7bdA9VEfMWWRL/evDxJmnX4p+AjBc7mklqZ5F2pYsY6XXQuuS+2Sy+lnxz01kLg+RC4Cpv5dyYfK3h0j8KeyK8IuixycVONWVe9rANq8UaIsMrRN+6uDSC8zXiH4P+h6UDMm3jetc2ZyAfhBA8OyIs5QEShae2Rd7Y3WFJxBp6UVgyj6SkXGxrEdb1ZJgTTl4dyqiP0bYrLePNP2qSJ6OTfNdG791HF077uzXI96ABdMG54Wv9N9T/hmxKwV2Lajx/GZJMmHuwT9tkHKhkcxWea1HYam9QYSFUyJ5THfNk2A9u/r8DkL62MZ85zIQBisrlFjbPAGRejq6qyirBJPAy+FCjhM+oO/i2f2bGkkAfHGT0Og1BcrWVXs54yWdO7UZgie2F+Rmdwinb/GxebZJ+21ZQ4OkVr2t1Skr/PRni9+U7q/6xCLwUJgx45XJ0FE= 22 | sudo: false 23 | cache: 24 | directories: 25 | - node_modules 26 | addons: 27 | firefox: latest 28 | apt: 29 | sources: 30 | - google-chrome 31 | packages: 32 | - google-chrome-stable 33 | -------------------------------------------------------------------------------- /src/js/control-bar/time-controls/remaining-time-display.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file remaining-time-display.js 3 | */ 4 | import Component from '../../component.js'; 5 | import * as Dom from '../../utils/dom.js'; 6 | import formatTime from '../../utils/format-time.js'; 7 | 8 | /** 9 | * Displays the time left in the video 10 | * 11 | * @param {Player|Object} player 12 | * @param {Object=} options 13 | * @extends Component 14 | * @class RemainingTimeDisplay 15 | */ 16 | class RemainingTimeDisplay extends Component { 17 | 18 | constructor(player, options){ 19 | super(player, options); 20 | 21 | this.on(player, 'timeupdate', this.updateContent); 22 | } 23 | 24 | /** 25 | * Create the component's DOM element 26 | * 27 | * @return {Element} 28 | * @method createEl 29 | */ 30 | createEl() { 31 | let el = super.createEl('div', { 32 | className: 'vjs-remaining-time vjs-time-control vjs-control' 33 | }); 34 | 35 | this.contentEl_ = Dom.createEl('div', { 36 | className: 'vjs-remaining-time-display', 37 | // label the remaining time for screen reader users 38 | innerHTML: `${this.localize('Remaining Time')} -0:00`, 39 | }, { 40 | // tell screen readers not to automatically read the time as it changes 41 | 'aria-live': 'off' 42 | }); 43 | 44 | el.appendChild(this.contentEl_); 45 | return el; 46 | } 47 | 48 | /** 49 | * Update remaining time display 50 | * 51 | * @method updateContent 52 | */ 53 | updateContent() { 54 | if (this.player_.duration()) { 55 | const localizedText = this.localize('Remaining Time'); 56 | const formattedTime = formatTime(this.player_.remainingTime()); 57 | if (formattedTime !== this.formattedTime_) { 58 | this.formattedTime_ = formattedTime; 59 | this.contentEl_.innerHTML = `${localizedText} -${formattedTime}`; 60 | } 61 | } 62 | 63 | // Allows for smooth scrubbing, when player can't keep up. 64 | // var time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime(); 65 | // this.contentEl_.innerHTML = vjs.formatTime(time, this.player_.duration()); 66 | } 67 | 68 | } 69 | 70 | Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay); 71 | export default RemainingTimeDisplay; 72 | -------------------------------------------------------------------------------- /src/js/control-bar/text-track-controls/descriptions-button.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file descriptions-button.js 3 | */ 4 | import TextTrackButton from './text-track-button.js'; 5 | import Component from '../../component.js'; 6 | import * as Fn from '../../utils/fn.js'; 7 | 8 | /** 9 | * The button component for toggling and selecting descriptions 10 | * 11 | * @param {Object} player Player object 12 | * @param {Object=} options Object of option names and values 13 | * @param {Function=} ready Ready callback function 14 | * @extends TextTrackButton 15 | * @class DescriptionsButton 16 | */ 17 | class DescriptionsButton extends TextTrackButton { 18 | 19 | constructor(player, options, ready){ 20 | super(player, options, ready); 21 | this.el_.setAttribute('aria-label', 'Descriptions Menu'); 22 | 23 | let tracks = player.textTracks(); 24 | 25 | if (tracks) { 26 | let changeHandler = Fn.bind(this, this.handleTracksChange); 27 | 28 | tracks.addEventListener('change', changeHandler); 29 | this.on('dispose', function() { 30 | tracks.removeEventListener('change', changeHandler); 31 | }); 32 | } 33 | } 34 | 35 | /** 36 | * Handle text track change 37 | * 38 | * @method handleTracksChange 39 | */ 40 | handleTracksChange(event){ 41 | let tracks = this.player().textTracks(); 42 | let disabled = false; 43 | 44 | // Check whether a track of a different kind is showing 45 | for (let i = 0, l = tracks.length; i < l; i++) { 46 | let track = tracks[i]; 47 | if (track['kind'] !== this.kind_ && track['mode'] === 'showing') { 48 | disabled = true; 49 | break; 50 | } 51 | } 52 | 53 | // If another track is showing, disable this menu button 54 | if (disabled) { 55 | this.disable(); 56 | } else { 57 | this.enable(); 58 | } 59 | } 60 | 61 | /** 62 | * Allow sub components to stack CSS class names 63 | * 64 | * @return {String} The constructed class name 65 | * @method buildCSSClass 66 | */ 67 | buildCSSClass() { 68 | return `vjs-descriptions-button ${super.buildCSSClass()}`; 69 | } 70 | 71 | } 72 | 73 | DescriptionsButton.prototype.kind_ = 'descriptions'; 74 | DescriptionsButton.prototype.controlText_ = 'Descriptions'; 75 | 76 | Component.registerComponent('DescriptionsButton', DescriptionsButton); 77 | export default DescriptionsButton; 78 | -------------------------------------------------------------------------------- /docs/examples/elephantsdream/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Video.js Text Descriptions, Chapters & Captions Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 |

This page demonstrates a text descriptions track (intended primarily for blind and visually impaired consumers of visual media)

23 | 24 | 28 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/js/extend.js: -------------------------------------------------------------------------------- 1 | import log from './utils/log'; 2 | 3 | /* 4 | * @file extend.js 5 | * 6 | * A combination of node inherits and babel's inherits (after transpile). 7 | * Both work the same but node adds `super_` to the subClass 8 | * and Bable adds the superClass as __proto__. Both seem useful. 9 | */ 10 | const _inherits = function (subClass, superClass) { 11 | if (typeof superClass !== 'function' && superClass !== null) { 12 | throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); 13 | } 14 | 15 | subClass.prototype = Object.create(superClass && superClass.prototype, { 16 | constructor: { 17 | value: subClass, 18 | enumerable: false, 19 | writable: true, 20 | configurable: true 21 | } 22 | }); 23 | 24 | if (superClass) { 25 | // node 26 | subClass.super_ = superClass; 27 | } 28 | }; 29 | 30 | /* 31 | * Function for subclassing using the same inheritance that 32 | * videojs uses internally 33 | * ```js 34 | * var Button = videojs.getComponent('Button'); 35 | * ``` 36 | * ```js 37 | * var MyButton = videojs.extend(Button, { 38 | * constructor: function(player, options) { 39 | * Button.call(this, player, options); 40 | * }, 41 | * onClick: function() { 42 | * // doSomething 43 | * } 44 | * }); 45 | * ``` 46 | */ 47 | const extendFn = function(superClass, subClassMethods={}) { 48 | let subClass = function() { 49 | superClass.apply(this, arguments); 50 | }; 51 | let methods = {}; 52 | 53 | if (typeof subClassMethods === 'object') { 54 | if (typeof subClassMethods.init === 'function') { 55 | log.warn('Constructor logic via init() is deprecated; please use constructor() instead.'); 56 | subClassMethods.constructor = subClassMethods.init; 57 | } 58 | if (subClassMethods.constructor !== Object.prototype.constructor) { 59 | subClass = subClassMethods.constructor; 60 | } 61 | methods = subClassMethods; 62 | } else if (typeof subClassMethods === 'function') { 63 | subClass = subClassMethods; 64 | } 65 | 66 | _inherits(subClass, superClass); 67 | 68 | // Extend subObj's prototype with functions and other properties from props 69 | for (var name in methods) { 70 | if (methods.hasOwnProperty(name)) { 71 | subClass.prototype[name] = methods[name]; 72 | } 73 | } 74 | 75 | return subClass; 76 | }; 77 | 78 | export default extendFn; 79 | -------------------------------------------------------------------------------- /src/js/control-bar/progress-control/load-progress-bar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file load-progress-bar.js 3 | */ 4 | import Component from '../../component.js'; 5 | import * as Dom from '../../utils/dom.js'; 6 | 7 | /** 8 | * Shows load progress 9 | * 10 | * @param {Player|Object} player 11 | * @param {Object=} options 12 | * @extends Component 13 | * @class LoadProgressBar 14 | */ 15 | class LoadProgressBar extends Component { 16 | 17 | constructor(player, options){ 18 | super(player, options); 19 | this.on(player, 'progress', this.update); 20 | } 21 | 22 | /** 23 | * Create the component's DOM element 24 | * 25 | * @return {Element} 26 | * @method createEl 27 | */ 28 | createEl() { 29 | return super.createEl('div', { 30 | className: 'vjs-load-progress', 31 | innerHTML: `${this.localize('Loaded')}: 0%` 32 | }); 33 | } 34 | 35 | /** 36 | * Update progress bar 37 | * 38 | * @method update 39 | */ 40 | update() { 41 | let buffered = this.player_.buffered(); 42 | let duration = this.player_.duration(); 43 | let bufferedEnd = this.player_.bufferedEnd(); 44 | let children = this.el_.children; 45 | 46 | // get the percent width of a time compared to the total end 47 | let percentify = function (time, end){ 48 | let percent = (time / end) || 0; // no NaN 49 | return ((percent >= 1 ? 1 : percent) * 100) + '%'; 50 | }; 51 | 52 | // update the width of the progress bar 53 | this.el_.style.width = percentify(bufferedEnd, duration); 54 | 55 | // add child elements to represent the individual buffered time ranges 56 | for (let i = 0; i < buffered.length; i++) { 57 | let start = buffered.start(i); 58 | let end = buffered.end(i); 59 | let part = children[i]; 60 | 61 | if (!part) { 62 | part = this.el_.appendChild(Dom.createEl()); 63 | } 64 | 65 | // set the percent based on the width of the progress bar (bufferedEnd) 66 | part.style.left = percentify(start, bufferedEnd); 67 | part.style.width = percentify(end - start, bufferedEnd); 68 | } 69 | 70 | // remove unused buffered range elements 71 | for (let i = children.length; i > buffered.length; i--) { 72 | this.el_.removeChild(children[i-1]); 73 | } 74 | } 75 | 76 | } 77 | 78 | Component.registerComponent('LoadProgressBar', LoadProgressBar); 79 | export default LoadProgressBar; 80 | -------------------------------------------------------------------------------- /sandbox/descriptions.html.example: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Video.js Text Descriptions, Chapters & Captions Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 25 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/js/control-bar/control-bar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file control-bar.js 3 | */ 4 | import Component from '../component.js'; 5 | 6 | // Required children 7 | import PlayToggle from './play-toggle.js'; 8 | import CurrentTimeDisplay from './time-controls/current-time-display.js'; 9 | import DurationDisplay from './time-controls/duration-display.js'; 10 | import TimeDivider from './time-controls/time-divider.js'; 11 | import RemainingTimeDisplay from './time-controls/remaining-time-display.js'; 12 | import LiveDisplay from './live-display.js'; 13 | import ProgressControl from './progress-control/progress-control.js'; 14 | import FullscreenToggle from './fullscreen-toggle.js'; 15 | import VolumeControl from './volume-control/volume-control.js'; 16 | import VolumeMenuButton from './volume-menu-button.js'; 17 | import MuteToggle from './mute-toggle.js'; 18 | import ChaptersButton from './text-track-controls/chapters-button.js'; 19 | import DescriptionsButton from './text-track-controls/descriptions-button.js'; 20 | import SubtitlesButton from './text-track-controls/subtitles-button.js'; 21 | import CaptionsButton from './text-track-controls/captions-button.js'; 22 | import PlaybackRateMenuButton from './playback-rate-menu/playback-rate-menu-button.js'; 23 | import CustomControlSpacer from './spacer-controls/custom-control-spacer.js'; 24 | 25 | /** 26 | * Container of main controls 27 | * 28 | * @extends Component 29 | * @class ControlBar 30 | */ 31 | class ControlBar extends Component { 32 | 33 | /** 34 | * Create the component's DOM element 35 | * 36 | * @return {Element} 37 | * @method createEl 38 | */ 39 | createEl() { 40 | return super.createEl('div', { 41 | className: 'vjs-control-bar', 42 | dir: 'ltr' 43 | }, { 44 | 'role': 'group' // The control bar is a group, so it can contain menuitems 45 | }); 46 | } 47 | } 48 | 49 | ControlBar.prototype.options_ = { 50 | loadEvent: 'play', 51 | children: [ 52 | 'playToggle', 53 | 'volumeMenuButton', 54 | 'currentTimeDisplay', 55 | 'timeDivider', 56 | 'durationDisplay', 57 | 'progressControl', 58 | 'liveDisplay', 59 | 'remainingTimeDisplay', 60 | 'customControlSpacer', 61 | 'playbackRateMenuButton', 62 | 'chaptersButton', 63 | 'descriptionsButton', 64 | 'subtitlesButton', 65 | 'captionsButton', 66 | 'fullscreenToggle' 67 | ] 68 | }; 69 | 70 | Component.registerComponent('ControlBar', ControlBar); 71 | export default ControlBar; 72 | -------------------------------------------------------------------------------- /src/js/control-bar/time-controls/duration-display.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file duration-display.js 3 | */ 4 | import Component from '../../component.js'; 5 | import * as Dom from '../../utils/dom.js'; 6 | import formatTime from '../../utils/format-time.js'; 7 | 8 | /** 9 | * Displays the duration 10 | * 11 | * @param {Player|Object} player 12 | * @param {Object=} options 13 | * @extends Component 14 | * @class DurationDisplay 15 | */ 16 | class DurationDisplay extends Component { 17 | 18 | constructor(player, options){ 19 | super(player, options); 20 | 21 | // this might need to be changed to 'durationchange' instead of 'timeupdate' eventually, 22 | // however the durationchange event fires before this.player_.duration() is set, 23 | // so the value cannot be written out using this method. 24 | // Once the order of durationchange and this.player_.duration() being set is figured out, 25 | // this can be updated. 26 | this.on(player, 'timeupdate', this.updateContent); 27 | this.on(player, 'loadedmetadata', this.updateContent); 28 | } 29 | 30 | /** 31 | * Create the component's DOM element 32 | * 33 | * @return {Element} 34 | * @method createEl 35 | */ 36 | createEl() { 37 | let el = super.createEl('div', { 38 | className: 'vjs-duration vjs-time-control vjs-control' 39 | }); 40 | 41 | this.contentEl_ = Dom.createEl('div', { 42 | className: 'vjs-duration-display', 43 | // label the duration time for screen reader users 44 | innerHTML: `${this.localize('Duration Time')} 0:00` 45 | }, { 46 | // tell screen readers not to automatically read the time as it changes 47 | 'aria-live': 'off' 48 | }); 49 | 50 | el.appendChild(this.contentEl_); 51 | return el; 52 | } 53 | 54 | /** 55 | * Update duration time display 56 | * 57 | * @method updateContent 58 | */ 59 | updateContent() { 60 | let duration = this.player_.duration(); 61 | if (duration && this.duration_ !== duration) { 62 | this.duration_ = duration; 63 | let localizedText = this.localize('Duration Time'); 64 | let formattedTime = formatTime(duration); 65 | this.contentEl_.innerHTML = `${localizedText} ${formattedTime}`; // label the duration time for screen reader users 66 | } 67 | } 68 | 69 | } 70 | 71 | Component.registerComponent('DurationDisplay', DurationDisplay); 72 | export default DurationDisplay; 73 | -------------------------------------------------------------------------------- /test/unit/tracks/html-track-element-list.test.js: -------------------------------------------------------------------------------- 1 | import HTMLTrackElement from '../../../src/js/tracks/html-track-element.js'; 2 | import HTMLTrackElementList from '../../../src/js/tracks/html-track-element-list.js'; 3 | import TextTrack from '../../../src/js/tracks/text-track.js'; 4 | 5 | const defaultTech = { 6 | textTracks() {}, 7 | on() {}, 8 | off() {}, 9 | currentTime() {} 10 | }; 11 | 12 | const track1 = new TextTrack({ 13 | id: 1, 14 | tech: defaultTech 15 | }); 16 | const track2 = new TextTrack({ 17 | id: 2, 18 | tech: defaultTech 19 | }); 20 | 21 | const genericHtmlTrackElements = [{ 22 | tech() {}, 23 | kind: 'captions', 24 | track: track1 25 | }, { 26 | tech() {}, 27 | kind: 'chapters', 28 | track: track2 29 | }]; 30 | 31 | q.module('HTML Track Element List'); 32 | 33 | test('HTMLTrackElementList\'s length is set correctly', function() { 34 | let htmlTrackElementList = new HTMLTrackElementList(genericHtmlTrackElements); 35 | 36 | equal(htmlTrackElementList.length, genericHtmlTrackElements.length, `the length is ${genericHtmlTrackElements.length}`); 37 | }); 38 | 39 | test('can get html track element by track', function() { 40 | let htmlTrackElementList = new HTMLTrackElementList(genericHtmlTrackElements); 41 | 42 | equal(htmlTrackElementList.getTrackElementByTrack_(track1).kind, 'captions', 'track1 has kind of captions'); 43 | equal(htmlTrackElementList.getTrackElementByTrack_(track2).kind, 'chapters', 'track2 has kind of captions'); 44 | }); 45 | 46 | test('length is updated when new tracks are added or removed', function() { 47 | let htmlTrackElementList = new HTMLTrackElementList(genericHtmlTrackElements); 48 | 49 | htmlTrackElementList.addTrackElement_({tech() {}}); 50 | equal(htmlTrackElementList.length, genericHtmlTrackElements.length + 1, `the length is ${genericHtmlTrackElements.length + 1}`); 51 | htmlTrackElementList.addTrackElement_({tech() {}}); 52 | equal(htmlTrackElementList.length, genericHtmlTrackElements.length + 2, `the length is ${genericHtmlTrackElements.length + 2}`); 53 | 54 | htmlTrackElementList.removeTrackElement_(htmlTrackElementList.getTrackElementByTrack_(track1)); 55 | equal(htmlTrackElementList.length, genericHtmlTrackElements.length + 1, `the length is ${genericHtmlTrackElements.length + 1}`); 56 | htmlTrackElementList.removeTrackElement_(htmlTrackElementList.getTrackElementByTrack_(track2)); 57 | equal(htmlTrackElementList.length, genericHtmlTrackElements.length, `the length is ${genericHtmlTrackElements.length}`); 58 | }); 59 | -------------------------------------------------------------------------------- /src/js/menu/menu-item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file menu-item.js 3 | */ 4 | import ClickableComponent from '../clickable-component.js'; 5 | import Component from '../component.js'; 6 | import assign from 'object.assign'; 7 | 8 | /** 9 | * The component for a menu item. `
  • ` 10 | * 11 | * @param {Player|Object} player 12 | * @param {Object=} options 13 | * @extends Button 14 | * @class MenuItem 15 | */ 16 | class MenuItem extends ClickableComponent { 17 | 18 | constructor(player, options) { 19 | super(player, options); 20 | 21 | this.selectable = options['selectable']; 22 | 23 | this.selected(options['selected']); 24 | 25 | if (this.selectable) { 26 | // TODO: May need to be either menuitemcheckbox or menuitemradio, 27 | // and may need logical grouping of menu items. 28 | this.el_.setAttribute('role', 'menuitemcheckbox'); 29 | } else { 30 | this.el_.setAttribute('role', 'menuitem'); 31 | } 32 | } 33 | 34 | /** 35 | * Create the component's DOM element 36 | * 37 | * @param {String=} type Desc 38 | * @param {Object=} props Desc 39 | * @return {Element} 40 | * @method createEl 41 | */ 42 | createEl(type, props, attrs) { 43 | return super.createEl('li', assign({ 44 | className: 'vjs-menu-item', 45 | innerHTML: this.localize(this.options_['label']), 46 | tabIndex: -1 47 | }, props), attrs); 48 | } 49 | 50 | /** 51 | * Handle a click on the menu item, and set it to selected 52 | * 53 | * @method handleClick 54 | */ 55 | handleClick() { 56 | this.selected(true); 57 | } 58 | 59 | /** 60 | * Set this menu item as selected or not 61 | * 62 | * @param {Boolean} selected 63 | * @method selected 64 | */ 65 | selected(selected) { 66 | if (this.selectable) { 67 | if (selected) { 68 | this.addClass('vjs-selected'); 69 | this.el_.setAttribute('aria-checked','true'); 70 | // aria-checked isn't fully supported by browsers/screen readers, 71 | // so indicate selected state to screen reader in the control text. 72 | this.controlText(', selected'); 73 | } else { 74 | this.removeClass('vjs-selected'); 75 | this.el_.setAttribute('aria-checked','false'); 76 | // Indicate un-selected state to screen reader 77 | // Note that a space clears out the selected state text 78 | this.controlText(' '); 79 | } 80 | } 81 | } 82 | } 83 | 84 | Component.registerComponent('MenuItem', MenuItem); 85 | export default MenuItem; 86 | -------------------------------------------------------------------------------- /src/js/utils/browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file browser.js 3 | */ 4 | import document from 'global/document'; 5 | import window from 'global/window'; 6 | 7 | const USER_AGENT = window.navigator.userAgent; 8 | const webkitVersionMap = (/AppleWebKit\/([\d.]+)/i).exec(USER_AGENT); 9 | const appleWebkitVersion = webkitVersionMap ? parseFloat(webkitVersionMap.pop()) : null; 10 | 11 | /* 12 | * Device is an iPhone 13 | * 14 | * @type {Boolean} 15 | * @constant 16 | * @private 17 | */ 18 | export const IS_IPAD = (/iPad/i).test(USER_AGENT); 19 | 20 | // The Facebook app's UIWebView identifies as both an iPhone and iPad, so 21 | // to identify iPhones, we need to exclude iPads. 22 | // http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/ 23 | export const IS_IPHONE = (/iPhone/i).test(USER_AGENT) && !IS_IPAD; 24 | export const IS_IPOD = (/iPod/i).test(USER_AGENT); 25 | export const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD; 26 | 27 | export const IOS_VERSION = (function(){ 28 | var match = USER_AGENT.match(/OS (\d+)_/i); 29 | if (match && match[1]) { return match[1]; } 30 | })(); 31 | 32 | export const IS_ANDROID = (/Android/i).test(USER_AGENT); 33 | export const ANDROID_VERSION = (function() { 34 | // This matches Android Major.Minor.Patch versions 35 | // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned 36 | var match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i), 37 | major, 38 | minor; 39 | 40 | if (!match) { 41 | return null; 42 | } 43 | 44 | major = match[1] && parseFloat(match[1]); 45 | minor = match[2] && parseFloat(match[2]); 46 | 47 | if (major && minor) { 48 | return parseFloat(match[1] + '.' + match[2]); 49 | } else if (major) { 50 | return major; 51 | } else { 52 | return null; 53 | } 54 | })(); 55 | // Old Android is defined as Version older than 2.3, and requiring a webkit version of the android browser 56 | export const IS_OLD_ANDROID = IS_ANDROID && (/webkit/i).test(USER_AGENT) && ANDROID_VERSION < 2.3; 57 | export const IS_NATIVE_ANDROID = IS_ANDROID && ANDROID_VERSION < 5 && appleWebkitVersion < 537; 58 | 59 | export const IS_FIREFOX = (/Firefox/i).test(USER_AGENT); 60 | export const IS_CHROME = (/Chrome/i).test(USER_AGENT); 61 | export const IS_IE8 = (/MSIE\s8\.0/).test(USER_AGENT); 62 | 63 | export const TOUCH_ENABLED = !!(('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch); 64 | export const BACKGROUND_SIZE_SUPPORTED = 'backgroundSize' in document.createElement('video').style; 65 | -------------------------------------------------------------------------------- /src/js/tracks/text-track-cue-list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file text-track-cue-list.js 3 | */ 4 | import * as browser from '../utils/browser.js'; 5 | import document from 'global/document'; 6 | 7 | /** 8 | * A List of text track cues as defined in: 9 | * https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist 10 | * 11 | * interface TextTrackCueList { 12 | * readonly attribute unsigned long length; 13 | * getter TextTrackCue (unsigned long index); 14 | * TextTrackCue? getCueById(DOMString id); 15 | * }; 16 | * 17 | * @param {Array} cues A list of cues to be initialized with 18 | * @class TextTrackCueList 19 | */ 20 | 21 | class TextTrackCueList { 22 | constructor(cues) { 23 | let list = this; 24 | 25 | if (browser.IS_IE8) { 26 | list = document.createElement('custom'); 27 | 28 | for (let prop in TextTrackCueList.prototype) { 29 | if (prop !== 'constructor') { 30 | list[prop] = TextTrackCueList.prototype[prop]; 31 | } 32 | } 33 | } 34 | 35 | TextTrackCueList.prototype.setCues_.call(list, cues); 36 | 37 | Object.defineProperty(list, 'length', { 38 | get() { 39 | return this.length_; 40 | } 41 | }); 42 | 43 | if (browser.IS_IE8) { 44 | return list; 45 | } 46 | } 47 | 48 | /** 49 | * A setter for cues in this list 50 | * 51 | * @param {Array} cues an array of cues 52 | * @method setCues_ 53 | * @private 54 | */ 55 | setCues_(cues) { 56 | let oldLength = this.length || 0; 57 | let i = 0; 58 | let l = cues.length; 59 | 60 | this.cues_ = cues; 61 | this.length_ = cues.length; 62 | 63 | let defineProp = function(index) { 64 | if (!('' + index in this)) { 65 | Object.defineProperty(this, '' + index, { 66 | get() { 67 | return this.cues_[index]; 68 | } 69 | }); 70 | } 71 | }; 72 | 73 | if (oldLength < l) { 74 | i = oldLength; 75 | 76 | for (; i < l; i++) { 77 | defineProp.call(this, i); 78 | } 79 | } 80 | } 81 | 82 | /** 83 | * Get a cue that is currently in the Cue list by id 84 | * 85 | * @param {String} id 86 | * @method getCueById 87 | * @return {Object} a single cue 88 | */ 89 | getCueById(id) { 90 | let result = null; 91 | 92 | for (let i = 0, l = this.length; i < l; i++) { 93 | let cue = this[i]; 94 | 95 | if (cue.id === id) { 96 | result = cue; 97 | break; 98 | } 99 | } 100 | 101 | return result; 102 | } 103 | } 104 | 105 | export default TextTrackCueList; 106 | -------------------------------------------------------------------------------- /src/js/media-error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file media-error.js 3 | */ 4 | import assign from 'object.assign'; 5 | 6 | /* 7 | * Custom MediaError to mimic the HTML5 MediaError 8 | * 9 | * @param {Number} code The media error code 10 | */ 11 | let MediaError = function(code){ 12 | if (typeof code === 'number') { 13 | this.code = code; 14 | } else if (typeof code === 'string') { 15 | // default code is zero, so this is a custom error 16 | this.message = code; 17 | } else if (typeof code === 'object') { // object 18 | assign(this, code); 19 | } 20 | 21 | if (!this.message) { 22 | this.message = MediaError.defaultMessages[this.code] || ''; 23 | } 24 | }; 25 | 26 | /* 27 | * The error code that refers two one of the defined 28 | * MediaError types 29 | * 30 | * @type {Number} 31 | */ 32 | MediaError.prototype.code = 0; 33 | 34 | /* 35 | * An optional message to be shown with the error. 36 | * Message is not part of the HTML5 video spec 37 | * but allows for more informative custom errors. 38 | * 39 | * @type {String} 40 | */ 41 | MediaError.prototype.message = ''; 42 | 43 | /* 44 | * An optional status code that can be set by plugins 45 | * to allow even more detail about the error. 46 | * For example the HLS plugin might provide the specific 47 | * HTTP status code that was returned when the error 48 | * occurred, then allowing a custom error overlay 49 | * to display more information. 50 | * 51 | * @type {Array} 52 | */ 53 | MediaError.prototype.status = null; 54 | 55 | MediaError.errorTypes = [ 56 | 'MEDIA_ERR_CUSTOM', // = 0 57 | 'MEDIA_ERR_ABORTED', // = 1 58 | 'MEDIA_ERR_NETWORK', // = 2 59 | 'MEDIA_ERR_DECODE', // = 3 60 | 'MEDIA_ERR_SRC_NOT_SUPPORTED', // = 4 61 | 'MEDIA_ERR_ENCRYPTED' // = 5 62 | ]; 63 | 64 | MediaError.defaultMessages = { 65 | 1: 'You aborted the media playback', 66 | 2: 'A network error caused the media download to fail part-way.', 67 | 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.', 68 | 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.', 69 | 5: 'The media is encrypted and we do not have the keys to decrypt it.' 70 | }; 71 | 72 | // Add types as properties on MediaError 73 | // e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; 74 | for (var errNum = 0; errNum < MediaError.errorTypes.length; errNum++) { 75 | MediaError[MediaError.errorTypes[errNum]] = errNum; 76 | // values should be accessible on both the class and instance 77 | MediaError.prototype[MediaError.errorTypes[errNum]] = errNum; 78 | } 79 | 80 | export default MediaError; 81 | -------------------------------------------------------------------------------- /test/unit/tracks/html-track-element.test.js: -------------------------------------------------------------------------------- 1 | import HTMLTrackElement from '../../../src/js/tracks/html-track-element.js'; 2 | import TextTrack from '../../../src/js/tracks/text-track.js'; 3 | import window from 'global/window'; 4 | 5 | const defaultTech = { 6 | textTracks() {}, 7 | on() {}, 8 | off() {}, 9 | currentTime() {} 10 | }; 11 | 12 | q.module('HTML Track Element'); 13 | 14 | test('html track element requires a tech', function() { 15 | window.throws( 16 | function() { 17 | new HTMLTrackElement(); 18 | }, 19 | new Error('A tech was not provided.'), 20 | 'a tech is required for html track element' 21 | ); 22 | }); 23 | 24 | test('can create a html track element with various properties', function() { 25 | let kind = 'chapters'; 26 | let label = 'English'; 27 | let language = 'en'; 28 | let src = 'http://www.example.com'; 29 | 30 | let htmlTrackElement = new HTMLTrackElement({ 31 | kind, 32 | label, 33 | language, 34 | src, 35 | tech: defaultTech 36 | }); 37 | 38 | equal(typeof htmlTrackElement.default, 'undefined', 'we have a default'); 39 | equal(htmlTrackElement.kind, kind, 'we have a kind'); 40 | equal(htmlTrackElement.label, label, 'we have a label'); 41 | equal(htmlTrackElement.readyState, 0, 'we have a readyState'); 42 | equal(htmlTrackElement.src, src, 'we have a src'); 43 | equal(htmlTrackElement.srclang, language, 'we have a srclang'); 44 | equal(htmlTrackElement.track.cues, null, 'we have a track'); 45 | }); 46 | 47 | test('defaults when items not provided', function() { 48 | let htmlTrackElement = new HTMLTrackElement({ 49 | tech: defaultTech 50 | }); 51 | 52 | equal(typeof htmlTrackElement.default, 'undefined', 'we have a default'); 53 | equal(htmlTrackElement.kind, 'subtitles', 'we have a kind'); 54 | equal(htmlTrackElement.label, '', 'we have a label'); 55 | equal(htmlTrackElement.readyState, 0, 'we have a readyState'); 56 | equal(typeof htmlTrackElement.src, 'undefined', 'we have a src'); 57 | equal(htmlTrackElement.srclang, '', 'we have a srclang'); 58 | equal(htmlTrackElement.track.cues.length, 0, 'we have a track'); 59 | }); 60 | 61 | test('fires loadeddata when track cues become populated', function() { 62 | let changes = 0; 63 | let loadHandler; 64 | 65 | loadHandler = function() { 66 | changes++; 67 | }; 68 | 69 | let htmlTrackElement = new HTMLTrackElement({ 70 | tech() {} 71 | }); 72 | 73 | htmlTrackElement.addEventListener('load', loadHandler); 74 | 75 | // trigger loaded cues event 76 | htmlTrackElement.track.trigger('loadeddata'); 77 | 78 | equal(changes, 1, 'a loadeddata event trigger addEventListener'); 79 | equal(htmlTrackElement.readyState, 2, 'readyState is loaded'); 80 | }); 81 | -------------------------------------------------------------------------------- /src/js/control-bar/mute-toggle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mute-toggle.js 3 | */ 4 | import Button from '../button'; 5 | import Component from '../component'; 6 | import * as Dom from '../utils/dom.js'; 7 | 8 | /** 9 | * A button component for muting the audio 10 | * 11 | * @param {Player|Object} player 12 | * @param {Object=} options 13 | * @extends Button 14 | * @class MuteToggle 15 | */ 16 | class MuteToggle extends Button { 17 | 18 | constructor(player, options) { 19 | super(player, options); 20 | 21 | this.on(player, 'volumechange', this.update); 22 | 23 | // hide mute toggle if the current tech doesn't support volume control 24 | if (player.tech_ && player.tech_['featuresVolumeControl'] === false) { 25 | this.addClass('vjs-hidden'); 26 | } 27 | 28 | this.on(player, 'loadstart', function() { 29 | this.update(); // We need to update the button to account for a default muted state. 30 | 31 | if (player.tech_['featuresVolumeControl'] === false) { 32 | this.addClass('vjs-hidden'); 33 | } else { 34 | this.removeClass('vjs-hidden'); 35 | } 36 | }); 37 | } 38 | 39 | /** 40 | * Allow sub components to stack CSS class names 41 | * 42 | * @return {String} The constructed class name 43 | * @method buildCSSClass 44 | */ 45 | buildCSSClass() { 46 | return `vjs-mute-control ${super.buildCSSClass()}`; 47 | } 48 | 49 | /** 50 | * Handle click on mute 51 | * 52 | * @method handleClick 53 | */ 54 | handleClick() { 55 | this.player_.muted( this.player_.muted() ? false : true ); 56 | } 57 | 58 | /** 59 | * Update volume 60 | * 61 | * @method update 62 | */ 63 | update() { 64 | var vol = this.player_.volume(), 65 | level = 3; 66 | 67 | if (vol === 0 || this.player_.muted()) { 68 | level = 0; 69 | } else if (vol < 0.33) { 70 | level = 1; 71 | } else if (vol < 0.67) { 72 | level = 2; 73 | } 74 | 75 | // Don't rewrite the button text if the actual text doesn't change. 76 | // This causes unnecessary and confusing information for screen reader users. 77 | // This check is needed because this function gets called every time the volume level is changed. 78 | let toMute = this.player_.muted() ? 'Unmute' : 'Mute'; 79 | if (this.controlText() !== toMute) { 80 | this.controlText(toMute); 81 | } 82 | 83 | /* TODO improve muted icon classes */ 84 | for (var i = 0; i < 4; i++) { 85 | Dom.removeElClass(this.el_, `vjs-vol-${i}`); 86 | } 87 | Dom.addElClass(this.el_, `vjs-vol-${level}`); 88 | } 89 | 90 | } 91 | 92 | MuteToggle.prototype.controlText_ = 'Mute'; 93 | 94 | Component.registerComponent('MuteToggle', MuteToggle); 95 | export default MuteToggle; 96 | -------------------------------------------------------------------------------- /src/js/tracks/text-track-list-converter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utilities for capturing text track state and re-creating tracks 3 | * based on a capture. 4 | * 5 | * @file text-track-list-converter.js 6 | */ 7 | 8 | /** 9 | * Examine a single text track and return a JSON-compatible javascript 10 | * object that represents the text track's state. 11 | * @param track {TextTrackObject} the text track to query 12 | * @return {Object} a serializable javascript representation of the 13 | * @private 14 | */ 15 | let trackToJson_ = function(track) { 16 | let ret = ['kind', 'label', 'language', 'id', 17 | 'inBandMetadataTrackDispatchType', 18 | 'mode', 'src'].reduce((acc, prop, i) => { 19 | if (track[prop]) { 20 | acc[prop] = track[prop]; 21 | } 22 | 23 | return acc; 24 | }, { 25 | cues: track.cues && Array.prototype.map.call(track.cues, function(cue) { 26 | return { 27 | startTime: cue.startTime, 28 | endTime: cue.endTime, 29 | text: cue.text, 30 | id: cue.id 31 | }; 32 | }) 33 | }); 34 | 35 | return ret; 36 | }; 37 | 38 | /** 39 | * Examine a tech and return a JSON-compatible javascript array that 40 | * represents the state of all text tracks currently configured. The 41 | * return array is compatible with `jsonToTextTracks`. 42 | * @param tech {tech} the tech object to query 43 | * @return {Array} a serializable javascript representation of the 44 | * @function textTracksToJson 45 | */ 46 | let textTracksToJson = function(tech) { 47 | 48 | let trackEls = tech.$$('track'); 49 | 50 | let trackObjs = Array.prototype.map.call(trackEls, (t) => t.track); 51 | let tracks = Array.prototype.map.call(trackEls, function(trackEl) { 52 | let json = trackToJson_(trackEl.track); 53 | if (trackEl.src) { 54 | json.src = trackEl.src; 55 | } 56 | return json; 57 | }); 58 | 59 | return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function(track) { 60 | return trackObjs.indexOf(track) === -1; 61 | }).map(trackToJson_)); 62 | }; 63 | 64 | /** 65 | * Creates a set of remote text tracks on a tech based on an array of 66 | * javascript text track representations. 67 | * @param json {Array} an array of text track representation objects, 68 | * like those that would be produced by `textTracksToJson` 69 | * @param tech {tech} the tech to create text tracks on 70 | * @function jsonToTextTracks 71 | */ 72 | let jsonToTextTracks = function(json, tech) { 73 | json.forEach(function(track) { 74 | let addedTrack = tech.addRemoteTextTrack(track).track; 75 | if (!track.src && track.cues) { 76 | track.cues.forEach((cue) => addedTrack.addCue(cue)); 77 | } 78 | }); 79 | 80 | return tech.textTracks(); 81 | }; 82 | 83 | export default {textTracksToJson, jsonToTextTracks, trackToJson_}; 84 | --------------------------------------------------------------------------------