Comparar commits
54 Commits
| Autor | SHA1 | Data | |
|---|---|---|---|
| 7bd5839537 | |||
| 3c28e188b0 | |||
| 133087ebf5 | |||
| 5bde16a1bc | |||
| cb42fcfb02 | |||
| 5b8b41e546 | |||
| f87ada1ee0 | |||
| 19b429bddc | |||
| c8f5e3a3fc | |||
| 22aade1b61 | |||
| e34335b012 | |||
| 866a3f37d8 | |||
| 37a6811fb6 | |||
| 03529163b6 | |||
| 2e2ac6f870 | |||
| 4979ea78d5 | |||
| 3585af0fe2 | |||
| d7d7cfeb9f | |||
| 4c3b60c3b6 | |||
| c2545ddb6d | |||
| 05b39fe281 | |||
| 7b0d738e8f | |||
| 8e7d8cc959 | |||
| 8888e2b5b5 | |||
| 1463e50f7b | |||
| a0ba8e2360 | |||
| 924fb279ee | |||
| e642295468 | |||
| de3945db15 | |||
| 58f2349302 | |||
| e3424f6a4d | |||
| 34aab3f357 | |||
| b387437aed | |||
| 524f868e31 | |||
| 1ba1f5aabd | |||
| 49bed07039 | |||
| 7bafcc2a55 | |||
| 2037e18235 | |||
| 8d1653aebc | |||
| 8f07f5d57c | |||
| 0bba3196d8 | |||
| 091bdf9261 | |||
| 29ffbfbc87 | |||
| 57af15ce8b | |||
| ce6acc832a | |||
| 8622b2648e | |||
| 73b6316f3b | |||
| 844e4f0107 | |||
| b07143d276 | |||
| e12bedbb45 | |||
| c340dbcccd | |||
| 20141202c1 | |||
| 5377ffc176 | |||
| f8aed4dc32 |
+2
-2
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"presets": [ ["es2015", {"loose": true}] ],
|
||||
"plugins": ["transform-es3-property-literals", "transform-es3-member-expression-literals", "inline-json"]
|
||||
"presets": [ "es3", ["es2015", {"loose": true}] ],
|
||||
"plugins": ["inline-json"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,38 @@
|
||||
{
|
||||
"source": {
|
||||
"include": [ "src/js/" ],
|
||||
"includePattern": ".js$"
|
||||
},
|
||||
"opts": {
|
||||
"destination": "docs/api",
|
||||
"readme": "docs/index.md",
|
||||
"template": "node_modules/tui-jsdoc-template",
|
||||
"package": "package.json",
|
||||
"recurse": true,
|
||||
"tutorials": "docs/guides",
|
||||
"encoding": "utf8"
|
||||
},
|
||||
"templates": {
|
||||
"default": {
|
||||
"staticFiles": {
|
||||
"include": ["build/docs/"]
|
||||
}
|
||||
},
|
||||
"logo": {
|
||||
"url": "http://videojs.com/img/logo.png",
|
||||
"height": "30px",
|
||||
"width": "214px"
|
||||
},
|
||||
"name": "Video.js Documentation",
|
||||
"tabNames": {
|
||||
"tutorials": "Guides"
|
||||
},
|
||||
"footerText": "<span class='copyright'><a href='http://videojs.com'>Video.js</a> is a free and open source HTML5 video player. © <a href='https://brightcove.com' target='_blank'>Brightcove, Inc</a>. <a href='https://github.com/videojs/video.js/blob/master/LICENSE' class='button blue' target='_blank'>View license</a></span> <ul class='other-links'><li><a href='http://videojs.com' class='button white'><i class='fa fa-external-link'></i> Video.js</a></li> <li><a href='https://twitter.com/videojs' class='button white' target='_blank'><i class='fa fa-twitter'></i> @videojs</a></li> <li><a href='http://github.com/videojs/video.js' class='button white' target='_blank'><i class='fa fa-github-alt'></i> Source</a></li> </ul>",
|
||||
"css": [
|
||||
"styles/videojs.css",
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css"
|
||||
]
|
||||
},
|
||||
"plugins": ["plugins/markdown"],
|
||||
"markdown": {
|
||||
"tags": ["example"]
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#resizer,
|
||||
footer {
|
||||
background-color: #ECEEF1;
|
||||
color: #868688;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
padding: 3px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
footer .copyright {
|
||||
float: left;
|
||||
}
|
||||
|
||||
footer .other-links {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
float: right;
|
||||
}
|
||||
|
||||
footer .other-links li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
display: none;
|
||||
}
|
||||
+4
-4
@@ -263,25 +263,25 @@ module.exports = function(grunt) {
|
||||
options: {
|
||||
release: 'major'
|
||||
},
|
||||
src: ['package.json', 'component.json']
|
||||
src: ['package.json']
|
||||
},
|
||||
minor: {
|
||||
options: {
|
||||
release: 'minor'
|
||||
},
|
||||
src: ['package.json', 'component.json']
|
||||
src: ['package.json']
|
||||
},
|
||||
patch: {
|
||||
options: {
|
||||
release: 'patch'
|
||||
},
|
||||
src: ['package.json', 'component.json']
|
||||
src: ['package.json']
|
||||
},
|
||||
prerelease: {
|
||||
options: {
|
||||
release: 'prerelease'
|
||||
},
|
||||
src: ['package.json', 'component.json']
|
||||
src: ['package.json']
|
||||
},
|
||||
css: {
|
||||
options: {
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "video.js",
|
||||
"description": "An HTML5 and Flash video player with a common API and skin for both.",
|
||||
"version": "5.16.0",
|
||||
"keywords": [
|
||||
"videojs",
|
||||
"html5",
|
||||
"flash",
|
||||
"video",
|
||||
"player"
|
||||
],
|
||||
"scripts": ["dist/video-js/video.dev.js"],
|
||||
"styles": ["dist/video-js/video-js.css"],
|
||||
"files": ["dist/video-js/video-js.swf"],
|
||||
"fonts": [
|
||||
"dist/video-js/font/vjs.eot",
|
||||
"dist/video-js/font/vjs.svg",
|
||||
"dist/video-js/font/vjs.ttf",
|
||||
"dist/video-js/font/vjs.woff"
|
||||
],
|
||||
"main": "dist/video-js/video.dev.js"
|
||||
}
|
||||
@@ -10,13 +10,13 @@ The architecture of the Video.js player is centered around components. The `Play
|
||||
* [Basic Example](#basic-example)
|
||||
* [Using Options](#using-options)
|
||||
* [Event Listening](#event-listening)
|
||||
* [using on](#using-on)
|
||||
* [Using on](#using-on)
|
||||
* [Using off](#using-off)
|
||||
* [Using one](#using-one)
|
||||
* [Using trigger](#using-trigger)
|
||||
* [Default Component Tree](#default-component-tree)
|
||||
* [Specific Component Details](#specific-component-details)
|
||||
* [Progress Control](#progress-control)
|
||||
* [Volume Panel](#volume-panel)
|
||||
* [Text Track Settings](#text-track-settings)
|
||||
|
||||
## What is a Component?
|
||||
@@ -195,8 +195,8 @@ myComponent.trigger('eventType');
|
||||
// does nothing
|
||||
```
|
||||
|
||||
If myFunc gets excluded, *all* listeners for the event type will get removed. If
|
||||
eventType gets excluded, *all* listeners will get removed from the component.
|
||||
If myFunc gets excluded, _all_ listeners for the event type will get removed. If
|
||||
eventType gets excluded, _all_ listeners will get removed from the component.
|
||||
You can use `off` to remove listeners that get added to other elements or
|
||||
components using:
|
||||
|
||||
@@ -316,24 +316,22 @@ Player
|
||||
|
||||
## Specific Component Details
|
||||
|
||||
### Progress Control
|
||||
### Volume Panel
|
||||
|
||||
The progress control has a grandchild component, the mouse time display, which shows a time tooltip that follows the mouse cursor.
|
||||
The `VolumePanel` includes the `MuteToggle` and the `VolumeControl` Components, which will be hidden if volume changes are not supported. There is one important option for the `VolumePanel` which can make your `VolumeControl` appear vertically over the `MuteToggle`. This can be set by passing `VolumePanel` `{inline: false}` as the default behavior is a horizontal `VolumeControl` with `{inline: true}`.
|
||||
|
||||
By default, the progress control is sandwiched inside the control bar between the volume menu button and the remaining time display. Some skins attempt to move the it above the control bar and have it span the full width of the player. In these cases, it is less than ideal to have the tooltips leave the bounds of the player. This can be prevented by setting the `keepTooltipsInside` option on the progress control.
|
||||
Example of a vertical `VolumeControl`
|
||||
|
||||
```js
|
||||
let player = videojs('myplayer', {
|
||||
controlBar: {
|
||||
progressControl: {
|
||||
keepTooltipsInside: true
|
||||
volumePanel: {
|
||||
inline: false
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
> **Note:** This makes the tooltips use a real element instead of pseudo-elements so targeting them with CSS is different.
|
||||
|
||||
### Text Track Settings
|
||||
|
||||
The text track settings component is only available when using emulated text tracks.
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# Debugging
|
||||
|
||||
## Table of Contents
|
||||
|
||||
* [Logging](#logging)
|
||||
* [API Overview](#api-overview)
|
||||
* [Log Safely](#log-safely)
|
||||
* [Log Objects Usefully](#log-objects-usefully)
|
||||
* [Log Levels](#log-levels)
|
||||
* [Available Log Levels](#available-log-levels)
|
||||
* [History](#history)
|
||||
|
||||
## Logging
|
||||
|
||||
Video.js includes a lightweight wrapper - `videojs.log` - around a subset of [the `console` API][console]. The available methods are `videojs.log`, `videojs.log.warn`, and `videojs.log.error`.
|
||||
|
||||
### API Overview
|
||||
|
||||
Most of these methods should be fairly self-explanatory, but for complete details, see [the API docs][api].
|
||||
|
||||
| Method | Alias Of | Matching Level(s) |
|
||||
| ------------------------------- | --------------- | ----------------- |
|
||||
| `videojs.log()` | `console.log` | all |
|
||||
| `videojs.log.warn()` | `console.warn` | all, warn |
|
||||
| `videojs.log.error()` | `console.error` | all, warn, error |
|
||||
| `videojs.log.level()` | n/a | n/a |
|
||||
| `videojs.log.history()` | n/a | n/a |
|
||||
| `videojs.log.history.clear()` | n/a | n/a |
|
||||
| `videojs.log.history.disable()` | n/a | n/a |
|
||||
| `videojs.log.history.enable()` | n/a | n/a |
|
||||
|
||||
For descriptions of these features, please refer to the sections below.
|
||||
|
||||
### Log Safely
|
||||
|
||||
Unlike the `console`, it's safe to leave `videojs.log` calls in your code. They won't throw errors when the `console` doesn't exist.
|
||||
|
||||
### Log Objects Usefully
|
||||
|
||||
Similar to the `console`, any number of mixed-type values can be passed to `videojs.log` methods:
|
||||
|
||||
```js
|
||||
videojs.log('this is a string', {butThis: 'is an object'});
|
||||
```
|
||||
|
||||
However, certain browser consoles (namely, IE10 and lower) do not support non-string values. Video.js improves on this situation by passing objects through `JSON.stringify` before logging them in IE10 and below. In other words, instead of the above producing this:
|
||||
|
||||
```txt
|
||||
VIDEOJS: this is a string [object Object]
|
||||
```
|
||||
|
||||
it will produce this:
|
||||
|
||||
```txt
|
||||
VIDEOJS: this is a string {"butThis": "is an object"}
|
||||
```
|
||||
|
||||
### Log Levels
|
||||
|
||||
Unlike the `console`, `videojs.log` includes the concept of logging levels. These levels toggle logging methods on or off.
|
||||
|
||||
Levels are exposed through the `videojs.log.level` method. This method acts as both a getter and setter for the current logging level. With no arguments, it returns the current logging level:
|
||||
|
||||
```js
|
||||
videojs.log.level(); // "all"
|
||||
```
|
||||
|
||||
By passing a string, the logging level can be changed to one of the available logging levels:
|
||||
|
||||
```js
|
||||
videojs.log.level('error'); // show only error messages and suppress others
|
||||
videojs.log('foo'); // does nothing
|
||||
videojs.log.warn('foo'); // does nothing
|
||||
videojs.log.error('foo'); // logs "foo" as an error
|
||||
```
|
||||
|
||||
### Available Log Levels
|
||||
|
||||
* **all** (default): enables all logging methods
|
||||
* **error**: only show `log.error` messages
|
||||
* **off**: disable all logging methods
|
||||
* **warn**: only show `log.warn` _and_ `log.error` messages
|
||||
|
||||
### History
|
||||
|
||||
> **Note:** In Video.js 5, `videojs.log.history` was an array. As of Video.js 6, it is a function which returns an array. This change was made to provide a richer, safer logging history API.
|
||||
|
||||
By default, the `videojs.log` module tracks a history of _everything_ passed to it regardless of logging level:
|
||||
|
||||
```js
|
||||
videojs.log.history(); // an array of everything that's been logged up to now
|
||||
```
|
||||
|
||||
This will work even when logging is set to **off**.
|
||||
|
||||
This can be useful, but it can also be a source of memory leaks. For example, logged objects will be retained in history even if references are removed everywhere else!
|
||||
|
||||
To avoid this problem, history can be disabled or enabled via method calls (using the `disable` and `enable` methods respectively). Disabling history is as easy as:
|
||||
|
||||
```js
|
||||
videojs.log.history.disable();
|
||||
```
|
||||
|
||||
Finally, the history (if enabled) can be cleared at any time via:
|
||||
|
||||
```js
|
||||
videojs.log.history.clear();
|
||||
```
|
||||
|
||||
[api]: http://docs.videojs.com/docs/api/index.html
|
||||
|
||||
[console]: https://developer.mozilla.org/en-US/docs/Web/API/Console
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
* [Overview](#overview)
|
||||
* [on() and addEventListener()](#on-and-addeventlistener)
|
||||
* [off() and removeEventListener](#off-and-removeeventlistener)
|
||||
* [off() and removeEventListener()](#off-and-removeeventlistener)
|
||||
* [one()](#one)
|
||||
* [trigger() and dispatchEvent](#trigger-and-dispatchevent)
|
||||
* [trigger() and dispatchEvent()](#trigger-and-dispatchevent)
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* [Q: How can I autoplay a video on a mobile device?](#q-how-can-i-autoplay-a-video-on-a-mobile-device)
|
||||
* [Q: How can I play RTMP video in video.js?](#q-how-can-i-play-rtmp-video-in-videojs)
|
||||
* [Q: How can I hide the links to my video/subtitles/audio/tracks?](#q-how-can-i-hide-the-links-to-my-videosubtitlesaudiotracks)
|
||||
* [Q: Can I turn off video.js logging?](#q-can-i-turn-off-videojs-logging)
|
||||
* [Q: What is a plugin?](#q-what-is-a-plugin)
|
||||
* [Q: How do I make a plugin for video.js?](#q-how-do-i-make-a-plugin-for-videojs)
|
||||
* [Q: Where can I find a list of video.js plugins?](#q-where-can-i-find-a-list-of-videojs-plugins)
|
||||
@@ -37,6 +38,7 @@
|
||||
* [Q: Does video.js have any support for advertisement integrations?](#q-does-videojs-have-any-support-for-advertisement-integrations)
|
||||
* [Q: Can video.js be required in node.js?](#q-can-videojs-be-required-in-nodejs)
|
||||
* [Q: Does video.js work with webpack?](#q-does-videojs-work-with-webpack)
|
||||
* [Q: Does video.js work with react?](#q-does-videojs-work-with-react)
|
||||
|
||||
## Q: What is video.js?
|
||||
|
||||
@@ -169,6 +171,16 @@ help but are outside of the scope of video.js.
|
||||
|
||||
For content that must be highly secure [videojs-contrib-eme][eme] adds DRM support.
|
||||
|
||||
## Q: Can I turn off video.js logging?
|
||||
|
||||
Yes! This can be achieved by adding the following code _after_ including Video.js, but _before_ creating any player(s):
|
||||
|
||||
```js
|
||||
videojs.log.level('off');
|
||||
```
|
||||
|
||||
For more information, including which logging levels are available, check out the [debugging guide][debug-guide].
|
||||
|
||||
## Q: What is a plugin?
|
||||
|
||||
A plugin is a group of reusable functionality that can be re-used by others. For instance a plugin could add
|
||||
@@ -258,6 +270,12 @@ Yes! Please [submit an issue or open a pull request][pr-issue-question] if this
|
||||
|
||||
Yes! Please [submit an issue or open a pull request][pr-issue-question] if this does not work.
|
||||
|
||||
Be sure to use `require('!style-loader!css-loader!video.js/dist/video-js.css')` to inject video.js CSS.
|
||||
|
||||
## Q: Does video.js work with react?
|
||||
|
||||
Yes! See [ReactJS integration example](./guides/react.md).
|
||||
|
||||
[plugin-guide]: plugins.md
|
||||
|
||||
[install-guide]: http://videojs.com/getting-started/
|
||||
@@ -307,3 +325,5 @@ Yes! Please [submit an issue or open a pull request][pr-issue-question] if this
|
||||
[semver]: http://semver.org/
|
||||
|
||||
[starter-example]: http://jsbin.com/axedog/edit?html,output
|
||||
|
||||
[debug-guide]: ./guides/debug.md
|
||||
@@ -0,0 +1,78 @@
|
||||
# Using the Modal Dialog Component
|
||||
|
||||
The `ModalDialog` component is part of Video.js core and provides a baked-in UI for full-player overlays.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
* [Creating the ModalDialog](#creating-the-modaldialog)
|
||||
* [Example Using createModal()](#example-using-createmodal)
|
||||
* [Example Using the ModalDialog Constructor](#example-using-the-modaldialog-constructor)
|
||||
* [Styling Modals Independently](#styling-modals-independently)
|
||||
|
||||
## Creating a ModalDialog
|
||||
|
||||
Aside from the [built-in Video.js component-creation methods][creating-component], the player includes a `createModal()` helper method.
|
||||
|
||||
We'll demonstrate both approaches in this document by creating a modal that opens when the player becomes paused and resumes playback when it is closed.
|
||||
|
||||
### Example Using `createModal()`
|
||||
|
||||
The `createModal()` method is intended for creating one-off modals that need to open for some temporary purpose. Therefore, they open themselves immediately upon creation and, by default, dispose themselves immediately upon closing.
|
||||
|
||||
```js
|
||||
var player = videojs('my-player');
|
||||
|
||||
player.on('pause', function() {
|
||||
|
||||
// Modals are temporary by default. They dispose themselves when they are
|
||||
// closed; so, we can create a new one each time the player is paused and
|
||||
// not worry about leaving extra nodes hanging around.
|
||||
var modal = player.createModal('This is a modal!');
|
||||
|
||||
// When the modal closes, resume playback.
|
||||
modal.on('modalclose', function() {
|
||||
player.play();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The `createModal()` method also takes a second argument - an object containing options for the modal. Refer to [the API documentation][api-doc] for a full set of options.
|
||||
|
||||
### Example Using the `ModalDialog` Constructor
|
||||
|
||||
Unlike when using `createModal()`, a modal created with any of the [common component creation methods][creating-component] _does not_ open by default. This makes this approach better suited to modals that are expected to live in the DOM indefinitely.
|
||||
|
||||
```js
|
||||
var player = videojs('my-player');
|
||||
var ModalDialog = videojs.getComponent('ModalDialog');
|
||||
|
||||
var modal = new ModalDialog(player, {
|
||||
|
||||
// We don't want this modal to go away when it closes.
|
||||
temporary: false
|
||||
});
|
||||
|
||||
player.addChild(modal);
|
||||
|
||||
player.on('pause', function() {
|
||||
modal.open();
|
||||
});
|
||||
|
||||
player.on('play', function() {
|
||||
modal.close();
|
||||
});
|
||||
```
|
||||
|
||||
Both of these examples are equivalent when it comes to the user's experience. Implementors should use whichever better suits their use-case.
|
||||
|
||||
## Styling Modals Independently
|
||||
|
||||
A common need for modals is to style them independently from one another. The recommended approach for this is to add a custom class to your modal and target that using CSS:
|
||||
|
||||
```js
|
||||
modal.addClass('vjs-my-fancy-modal');
|
||||
```
|
||||
|
||||
[api-doc]: http://docs.videojs.com/docs/api/modal-dialog.html
|
||||
|
||||
[creating-component]: ./components.md#creating-a-component
|
||||
@@ -367,6 +367,8 @@ Coming soon...
|
||||
|
||||
### React
|
||||
|
||||
See [ReactJS integration example](./react.md)
|
||||
|
||||
### Ember
|
||||
|
||||
### Angular
|
||||
|
||||
+296
-27
@@ -1,58 +1,327 @@
|
||||
# Plugins
|
||||
# Video.js Plugins
|
||||
|
||||
If you've built something cool with Video.js, you can easily share it with the rest of the world by creating a plugin. Although, you can roll your own, you can also use [generator-videojs-plugin](https://github.com/dmlap/generator-videojs-plugin), a [Yeoman](http://yeoman.io) generator that provides scaffolding for video.js plugins including:
|
||||
One of the great strengths of Video.js is its ecosystem of plugins that allow authors from all over the world to share their video player customizations. This includes everything from the simplest UI tweaks to new [playback technologies and source handlers](tech.md)!
|
||||
|
||||
* [Grunt](http://gruntjs.com) for build management
|
||||
* [npm](https://www.npmjs.org) for dependency management
|
||||
* [QUnit](http://qunitjs.com) for testing
|
||||
Because we view plugins as such an important part of Video.js, the organization is committed to maintaining a robust set of tools for plugin authorship:
|
||||
|
||||
## Step 1: Write Some Javascript
|
||||
* [generator-videojs-plugin][generator]
|
||||
|
||||
You may have already done this step. Code up something interesting and then wrap it in a function. At the most basic level, that's all a video.js plugin is. By convention, plugins take a hash of options as their first argument:
|
||||
A [Yeoman][yeoman] generator for scaffolding a Video.js plugin project. Additionally, it offers a set of [conventions for plugin authorship][standards] that, if followed, make authorship, contribution, and usage consistent and predictable.
|
||||
|
||||
In short, the generator sets up plugin authors to focus on writing their plugin - not messing with tools.
|
||||
|
||||
* [videojs-spellbook][spellbook]
|
||||
|
||||
As of version 3, the plugin generator includes a new dependency: [videojs-spellbook][spellbook]. Spellbook is a kitchen sink plugin development tool: it builds plugins, creates tags, runs a development server, and more.
|
||||
|
||||
The benefit of Spellbook is that you can run the generator _once_ and receive updates and bugfixes in Spellbook without having to run the generator again and deal with Yeoman conflicts and other headaches.
|
||||
|
||||
As long as your plugin project follows the [conventions][standards], Spellbook should work on it!
|
||||
|
||||
## Writing a Basic Plugin
|
||||
|
||||
If you've written a Video.js plugin before, the basic plugin concept should be familiar. It's similar to a jQuery plugin in that the core idea is that you're adding a method to the player.
|
||||
|
||||
### Write a JavaScript Function
|
||||
|
||||
A basic plugin is a plain JavaScript function:
|
||||
|
||||
```js
|
||||
function examplePlugin(options) {
|
||||
this.on('play', function(e) {
|
||||
console.log('playback has started!');
|
||||
|
||||
if (options.customClass) {
|
||||
this.addClass(options.customClass);
|
||||
}
|
||||
|
||||
this.on('playing', function() {
|
||||
videojs.log('playback began!');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
By convention, plugins are passed an `options` object; however, you can realistically accept whatever arguments you want. This example plugin will add a custom class (whatever is passed in as `options.customClass`) and, whenever playback begins, it will log a message to the browser console.
|
||||
|
||||
> **Note:** The value of `this` in the plugin function is the player instance; so, you have access to [its complete API][api-player].
|
||||
|
||||
### Register a Basic Plugin
|
||||
|
||||
Now that we have a function that does something with a player, all that's left is to register the plugin with Video.js:
|
||||
|
||||
```js
|
||||
videojs.registerPlugin('examplePlugin', examplePlugin);
|
||||
```
|
||||
|
||||
After that, any player will automatically have an `examplePlugin` method on its prototype!
|
||||
|
||||
> **Note:** The only stipulation with the name of the plugin is that it cannot conflict with any existing plugin or player method.
|
||||
|
||||
## Writing an Advanced Plugin
|
||||
|
||||
Video.js 6 introduces advanced plugins: these are plugins that share a similar API with basic plugins, but are class-based and offer a range of extra features out of the box.
|
||||
|
||||
While reading the following sections, you may want to refer to the [Plugin API docs][api-plugin] for more detail.
|
||||
|
||||
### Write a JavaScript Class/Constructor
|
||||
|
||||
If you're familiar with creating [components](components.md), this process is similar. An advanced plugin starts with a JavaScript class (a.k.a. a constructor function).
|
||||
|
||||
If you're using ES6 already, you can use that syntax with your transpiler/language of choice (Babel, TypeScript, etc):
|
||||
|
||||
```js
|
||||
const Plugin = videojs.getPlugin('plugin');
|
||||
|
||||
class ExamplePlugin extends Plugin {
|
||||
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
|
||||
if (options.customClass) {
|
||||
player.addClass(options.customClass);
|
||||
}
|
||||
|
||||
player.on('playing', function() {
|
||||
videojs.log('playback began!');
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or with ES5:
|
||||
|
||||
```js
|
||||
var Plugin = videojs.getPlugin('plugin');
|
||||
|
||||
var ExamplePlugin = videojs.extend(Plugin, {
|
||||
|
||||
constructor: function(player, options) {
|
||||
Plugin.call(this, player, options);
|
||||
|
||||
if (options.customClass) {
|
||||
player.addClass(options.customClass);
|
||||
}
|
||||
|
||||
player.on('playing', function() {
|
||||
videojs.log('playback began!');
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
For now, this example advanced plugin does the exact same thing as the basic plugin described above - not to worry, we will make it more interesting as we continue!
|
||||
|
||||
### Register a Advanced Plugin
|
||||
|
||||
The registration process for advanced plugins is identical to [the process for basic plugins](#register-a-basic-plugin).
|
||||
|
||||
```js
|
||||
videojs.registerPlugin('examplePlugin', ExamplePlugin);
|
||||
```
|
||||
|
||||
> **Note:** Because ES6 classes are syntactic sugar on top of existing constructor function and prototype architecture in JavaScript, in all cases `registerPlugin`'s second argument is a function.
|
||||
|
||||
### Key Differences from Basic Plugins
|
||||
|
||||
Advanced plugins have two key differences from basic plugins that are important to understand before describing their advanced features.
|
||||
|
||||
#### The Value of `this`
|
||||
|
||||
With basic plugins, the value of `this` in the plugin function will be the _player_.
|
||||
|
||||
With advanced plugins, the value of `this` is the _instance of the plugin class_. The player is passed to the plugin constructor as its first argument (and is automatically applied to the plugin instance as the `player` property) and any further arguments are passed after that.
|
||||
|
||||
#### The Player Plugin Name Property
|
||||
|
||||
Both basic plugins and advanced plugins are set up by calling a method on a player with a name matching the plugin (e.g., `player.examplePlugin()`).
|
||||
|
||||
However, with advanced plugins, this method acts like a factory function and it is _replaced_ for the current player by a new function which returns the plugin instance:
|
||||
|
||||
```js
|
||||
// `examplePlugin` has not been called, so it is a factory function.
|
||||
player.examplePlugin();
|
||||
|
||||
// `examplePlugin` is now a function that returns the same instance of
|
||||
// `ExamplePlugin` that was generated by the previous call.
|
||||
player.examplePlugin().someMethodName();
|
||||
```
|
||||
|
||||
With basic plugins, the method does not change - it is always the same function. It is up to the authors of basic plugins to deal with multiple calls to their plugin function.
|
||||
|
||||
### Features of Advanced Plugins
|
||||
|
||||
Up to this point, our example advanced plugin is functionally identical to our example basic plugin. However, advanced plugins bring with them a great deal of benefit that is not built into basic plugins.
|
||||
|
||||
#### Events
|
||||
|
||||
Like components, advanced plugins offer an implementation of events. This includes:
|
||||
|
||||
* The ability to listen for events on the plugin instance using `on` or `one`:
|
||||
|
||||
```js
|
||||
player.examplePlugin().on('example-event', function() {
|
||||
videojs.log('example plugin received an example-event');
|
||||
});
|
||||
```
|
||||
|
||||
* The ability to `trigger` custom events on a plugin instance:
|
||||
|
||||
```js
|
||||
player.examplePlugin().trigger('example-event');
|
||||
```
|
||||
|
||||
* The ability to stop listening to custom events on a plugin instance using `off`:
|
||||
|
||||
```js
|
||||
player.examplePlugin().off('example-event');
|
||||
```
|
||||
|
||||
By offering a built-in events system, advanced plugins offer a wider range of options for code structure with a pattern familiar to most web developers.
|
||||
|
||||
#### Statefulness
|
||||
|
||||
A new concept introduced for advanced plugins is _statefulness_. This is similar to React components' `state` property and `setState` method.
|
||||
|
||||
Advanced plugin instances each have a `state` property, which is a plain JavaScript object - it can contain any keys and values the plugin author wants.
|
||||
|
||||
A default `state` can be provided by adding a static property to a plugin constructor:
|
||||
|
||||
```js
|
||||
ExamplePlugin.defaultState = {
|
||||
customClass: 'default-custom-class'
|
||||
};
|
||||
```
|
||||
|
||||
When it's activated, `this` will be the Video.js player your plugin is attached to. You can use anything you'd like in the [Video.js API](./api.md) when you're writing a plugin: change the `src`, mess up the DOM, or listen for and emit your own events.
|
||||
|
||||
## Step 2: Registering A Plugin
|
||||
|
||||
It's time to give the rest of the world the opportunity to be awed by your genius. When your plugin is loaded, it needs to let Video.js know this amazing new functionality is now available:
|
||||
When the `state` is updated via the `setState` method, the plugin instance fires a `"statechanged"` event, but _only if something changed!_ This event can be used as a signal to update the DOM or perform some other action. The event object passed to listeners for this event includes, an object describing the changes that occurred on the `state` property:
|
||||
|
||||
```js
|
||||
videojs.plugin('examplePlugin', examplePlugin);
|
||||
player.examplePlugin().on('statechanged', function(e) {
|
||||
if (e.changes && e.changes.customClass) {
|
||||
this.player
|
||||
.removeClass(e.changes.customClass.from)
|
||||
.addClass(e.changes.customClass.to);
|
||||
}
|
||||
});
|
||||
|
||||
player.examplePlugin().setState({customClass: 'another-custom-class'});
|
||||
```
|
||||
|
||||
From this point on, your plugin will be added to the Video.js prototype and will show up as a property on every instance created. Make sure you choose a unique name that doesn't clash with any of the properties already in Video.js. Which leads us to...
|
||||
#### Lifecycle
|
||||
|
||||
## Step 3: Using A Plugin
|
||||
|
||||
There are two ways to initialize a plugin. If you're creating your video tag dynamically, you can specify the plugins you'd like to initialize with it and any options you want to pass to them:
|
||||
Like components, advanced plugins have a lifecycle. They can be created with their factory function and they can be destroyed using their `dispose` method:
|
||||
|
||||
```js
|
||||
videojs('vidId', {
|
||||
// set up a example plugin instance
|
||||
player.examplePlugin();
|
||||
|
||||
// dispose of it anytime thereafter
|
||||
player.examplePlugin().dispose();
|
||||
```
|
||||
|
||||
The `dispose` method has several effects:
|
||||
|
||||
* Triggers a `"dispose"` event on the plugin instance.
|
||||
* Cleans up all event listeners on the plugin instance, which helps avoid errors caused by events being triggered after an object is cleaned up.
|
||||
* Removes plugin state and references to the player to avoid memory leaks.
|
||||
* Reverts the player's named property (e.g. `player.examplePlugin`) _back_ to the original factory function, so the plugin can be set up again.
|
||||
|
||||
In addition, if the player is disposed, the disposal of all its advanced plugin instances will be triggered as well.
|
||||
|
||||
### Advanced Example Advanced Plugin
|
||||
|
||||
What follows is a complete ES6 advanced plugin that logs a custom message when the player's state changes between playing and paused. It uses all the described advanced features:
|
||||
|
||||
```js
|
||||
import videojs from 'video.js';
|
||||
|
||||
const Plugin = videojs.getPlugin('plugin');
|
||||
|
||||
class Advanced extends Plugin {
|
||||
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
|
||||
// Whenever the player emits a playing or paused event, we update the
|
||||
// state if necessary.
|
||||
this.on(player, ['playing', 'paused'], this.updateState);
|
||||
this.on('statechanged', this.logState);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
videojs.log('the advanced plugin is being disposed');
|
||||
}
|
||||
|
||||
updateState() {
|
||||
this.setState({playing: !player.paused()});
|
||||
}
|
||||
|
||||
logState(changed) {
|
||||
videojs.log(`the player is now ${this.state.playing ? 'playing' : 'paused'}`);
|
||||
}
|
||||
}
|
||||
|
||||
videojs.registerPlugin('advanced', Advanced);
|
||||
|
||||
const player = videojs('example-player');
|
||||
|
||||
player.advanced();
|
||||
|
||||
// This will begin playback, which will trigger a "playing" event, which will
|
||||
// update the state of the plugin, which will cause a message to be logged.
|
||||
player.play();
|
||||
|
||||
// This will pause playback, which will trigger a "paused" event, which will
|
||||
// update the state of the plugin, which will cause a message to be logged.
|
||||
player.pause();
|
||||
|
||||
player.advanced().dispose();
|
||||
|
||||
// This will begin playback, but the plugin has been disposed, so it will not
|
||||
// log any messages.
|
||||
player.play();
|
||||
```
|
||||
|
||||
This example may be a bit pointless in reality, but it demonstrates the sort of flexibility offered by advanced plugins over basic plugins.
|
||||
|
||||
## Setting up a Plugin
|
||||
|
||||
There are two ways to set up (or initialize) a plugin on a player. Both ways work identically for both basic and advanced plugins.
|
||||
|
||||
The first way is during creation of the player. Using the `plugins` option, a plugin can be automatically set up on a player:
|
||||
|
||||
```js
|
||||
videojs('example-player', {
|
||||
plugins: {
|
||||
examplePlugin: {
|
||||
exampleOption: true
|
||||
customClass: 'example-class'
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
If you've already initialized your video tag, you can activate a plugin at any time by calling its setup function directly:
|
||||
Otherwise, a plugin can be manually set up:
|
||||
|
||||
```js
|
||||
var video = videojs('cool-vid');
|
||||
video.examplePlugin({ exampleOption: true });
|
||||
var player = videojs('example-player');
|
||||
player.examplePlugin({customClass: 'example-class'});
|
||||
```
|
||||
|
||||
That's it. Head on over to the [Video.js wiki](https://github.com/videojs/video.js/wiki/Plugins) and add your plugin to the list so everyone else can check it out.
|
||||
These two methods are functionally identical - use whichever you prefer!
|
||||
|
||||
## How should I use the Video.js icons in my plugin?
|
||||
## References
|
||||
|
||||
If you'd like to use any of the icons available in the [Video.js icon set](http://videojs.github.io/font/), please target them via the CSS class names instead of codepoints. The codepoints _may_ change between versions of the font, so using the class names ensures that your plugin will stay up to date with any font changes.
|
||||
* [Player API][api-player]
|
||||
* [Plugin API][api-plugin]
|
||||
* [Plugin Generator][generator]
|
||||
* [Plugin Conventions][standards]
|
||||
|
||||
[api-player]: http://docs.videojs.com/docs/api/player.html
|
||||
|
||||
[api-plugin]: http://docs.videojs.com/docs/api/plugin.html
|
||||
|
||||
[generator]: https://github.com/videojs/generator-videojs-plugin
|
||||
|
||||
[spellbook]: https://github.com/videojs/spellbook
|
||||
|
||||
[standards]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/standards.md
|
||||
|
||||
[yeoman]: http://yeoman.io
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# video.js and ReactJS integration
|
||||
|
||||
Here's a basic ReactJS player implementation.
|
||||
|
||||
It just instantiates the video.js player on `componentDidMount` and destroys it on `componentWillUnmount`.
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import videojs from 'video.js'
|
||||
|
||||
export default class VideoPlayer extends React.Component {
|
||||
componentDidMount() {
|
||||
// instantiate video.js
|
||||
this.player = videojs(this.videoNode, this.props, function onPlayerReady() {
|
||||
console.log('onPlayerReady', this)
|
||||
});
|
||||
}
|
||||
|
||||
// destroy player on unmount
|
||||
componentWillUnmount() {
|
||||
if (this.player) {
|
||||
this.player.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
// wrap the player in a div with a `data-vjs-player` attribute
|
||||
// so videojs won't create additional wrapper in the DOM
|
||||
// see https://github.com/videojs/video.js/pull/3856
|
||||
render() {
|
||||
return (
|
||||
<div data-vjs-player>
|
||||
<video ref={ node => this.videoNode = node } className="video-js"></video>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can then use it like this:
|
||||
|
||||
```jsx
|
||||
// see https://github.com/videojs/video.js/blob/master/docs/guides/options.md
|
||||
const videoJsOptions = {
|
||||
autoPlay: true,
|
||||
controls: true,
|
||||
sources: [{
|
||||
src: '/path/to/video.mp4',
|
||||
type: 'video/mp4'
|
||||
}]
|
||||
}
|
||||
|
||||
return <VideoPlayer { ...videoJsOptions } />
|
||||
```
|
||||
|
||||
Dont forget to include the video.js CSS, located at `video.js/dist/video-js.css`.
|
||||
+65
-97
@@ -1,120 +1,88 @@
|
||||
# Playback Technology ("Tech")
|
||||
|
||||
Playback Technology refers to the specific browser or plugin technology used to play the video or audio. When using HTML5, the playback technology is the video or audio element. When using Flash, the playback technology is the specific Flash player used, e.g. Flowplayer, YouTube Player, video-js.swf, etc. (not just "Flash"). This could also include Silverlight, Quicktime, or any other plugin that will play back video in the browser, as long as there is an API wrapper written for it.
|
||||
Playback Technology refers to the specific browser or plugin technology used to play the video or audio.
|
||||
When using HTML5, the playback technology is the video or audio element. When using Flash and the
|
||||
[videojs-flash project](http://github.com/videojs/videojs-flash) (which was builtin to Video.js prior to Video.js 6), the playback technology is the specific Flash player used, and in most cases that will be [video-js-swf](https://github.com/videojs/video-js-swf). This could also include Silverlight,
|
||||
Quicktime, or any other plugin that will play back video in the browser, as long as there is an API wrapper written for it.
|
||||
|
||||
Essentially we're using HTML5 and plugins only as video decoders, and using HTML and JavaScript to create a consistent API and skinning experience across all of them.
|
||||
Essentially we're using HTML5 and plugins only as video decoders, and using HTML and JavaScript to create a
|
||||
consistent API and skinning experience across all of them.
|
||||
|
||||
## Building an API Wrapper
|
||||
## Table of Contents
|
||||
|
||||
We'll write a more complete guide on writing a wrapper soon, but for now the best resource is the [Video.js](https://github.com/zencoder/video-js/tree/master/src) source where you can see how both the HTML5 and video-js.swf API wrappers were created.
|
||||
* [Writing a new Tech](#writing-a-new-tech)
|
||||
* [Proritizing Techs using techOrder](#proritizing-techs-using-techorder)
|
||||
* [Resources](#resources)
|
||||
|
||||
## Required Methods
|
||||
## Writing a new Tech
|
||||
|
||||
canPlayType
|
||||
play
|
||||
pause
|
||||
currentTime
|
||||
volume
|
||||
duration
|
||||
buffered
|
||||
supportsFullScreen
|
||||
When writing a Tech you will have to integrate your technology's API with the existing API. In the following examples
|
||||
we are going to make a clone of the base `Tech` class which would not actually play back any media.
|
||||
|
||||
## Required Events
|
||||
|
||||
loadstart
|
||||
play
|
||||
pause
|
||||
playing
|
||||
ended
|
||||
volumechange
|
||||
durationchange
|
||||
error
|
||||
|
||||
## Optional Events (include if supported)
|
||||
|
||||
timeupdate
|
||||
progress
|
||||
enterFullScreen
|
||||
exitFullScreen
|
||||
|
||||
## Adding Playback Technology
|
||||
|
||||
When adding additional Tech to a video player, make sure to add the supported tech to the video object.
|
||||
|
||||
### Tag Method:
|
||||
|
||||
```html
|
||||
<video data-setup='{"techOrder": ["html5", "flash", "other supported tech"]}'>
|
||||
```
|
||||
|
||||
### Object Method:
|
||||
ES6 Example of a new Tech
|
||||
|
||||
```js
|
||||
videojs("videoID", {
|
||||
techOrder: ["html5", "flash", "other supported tech"]
|
||||
const Tech = videojs.getTech('Tech');
|
||||
class MyTech extnds Tech {
|
||||
constructor(options, ready) {
|
||||
super(options, ready);
|
||||
console.log('My new Tech!');
|
||||
}
|
||||
}
|
||||
|
||||
// Add our new Tech to the internal Tech list and to the end of the default tech order list
|
||||
// the techOrder will now look like this by default: `['html5', 'mytech']`
|
||||
videojs.registerTech('MyTech', MyTech);
|
||||
```
|
||||
|
||||
ES5 Example of a new Tech
|
||||
|
||||
```js
|
||||
var Tech = videojs.getTech('Tech');
|
||||
var MyTech = videojs.extend(Tech, {
|
||||
constructor: function(options, ready) {
|
||||
Tech.call(options, ready);
|
||||
console.log('My new Tech!');
|
||||
}
|
||||
});
|
||||
|
||||
// Add our new Tech to the internal Tech list and to the end of the default `techOrder` list
|
||||
videojs.registerTech('MyTech', MyTech);
|
||||
```
|
||||
|
||||
## Technology Ordering
|
||||
For more information on what methods and events a `Tech` is required to implement. See the [Html5 tech API](https://github.com/videojs/video.js/blob/master/src/js/tech/html5.js)
|
||||
|
||||
By default Video.js performs "Tech-first" ordering when it searches for a source/tech combination to play videos. This means that if you have two sources and two techs, video.js will try to play each video with the first tech in the `techOrder` option property before moving on to try the next playback technology.
|
||||
## Proritizing Techs using `techOrder`
|
||||
|
||||
Tech-first ordering can present a problem if you have a `sourceHandler` that supports both `Html5` and `Flash` techs such as videojs-contrib-hls.
|
||||
By default Video.js performs "Tech-first" ordering when it searches for a source/tech combination to play videos.
|
||||
This means that if you have two sources and two techs, video.js will try to play each video with the first tech in
|
||||
the `techOrder` option property before moving on to try the next playback technology. By default Techs will be added
|
||||
to the `techOrder` in the order in which they were registered to video.js. So in our `MyTech` example above it would
|
||||
look something like this:
|
||||
|
||||
For example, given the following video element:
|
||||
```json
|
||||
['Html5', 'MyTech']
|
||||
```
|
||||
|
||||
Tech-first is only a problem when you want to prioritize techs differently than the order of registration. To demonstrate how you would change that lets give some examples:
|
||||
|
||||
Example 1: Only use `MyTech`:
|
||||
|
||||
```js
|
||||
var player = videojs('some-player-id', {techOrder: ['MyTech']});
|
||||
```
|
||||
|
||||
Example 2: Use `MyTech` and fallback to `Html5` when `MyTech` cannot playback a video:
|
||||
|
||||
```html
|
||||
<video data-setup='{"techOrder": ["html5", "flash"]}'>
|
||||
<source src="http://your.static.provider.net/path/to/video.m3u8" type="application/x-mpegURL">
|
||||
<source src="http://your.static.provider.net/path/to/video.mp4" type="video/mp4">
|
||||
</video>
|
||||
var player = videojs('some-player-id', {techOrder: ['MyTech', 'Html5']});
|
||||
```
|
||||
|
||||
There is a good chance that the mp4 source will be selected on platforms that do not have media source extensions. Video.js will try all sources against the first playback technology, in this case `Html5`, and select the first source that can play - in this case MP4.
|
||||
> Note: each of these examples use JavaScript only, but it is possible to pass the `techOrder` using HTML by using the [`data-setup` attribute on a video](./setup.md#automatic-setup).
|
||||
|
||||
In "Tech-first" mode, the tests run something like this:
|
||||
Can video.m3u8 play with Html5? No...
|
||||
Can video.mp4 play with Html5? Yes! Use the second source.
|
||||
## Resources
|
||||
|
||||
Video.js now provides another method of selecting the source - "Source-first" ordering. In this mode, Video.js tries the first source against every tech in `techOrder` before moving onto the next source.
|
||||
Here are some examples of a `Tech` which you can use as resources when designing a new `Tech`:
|
||||
|
||||
With a player setup as follows:
|
||||
|
||||
```html
|
||||
<video data-setup='{"techOrder": ["html5", "flash"], "sourceOrder": true}'>
|
||||
<source src="http://your.static.provider.net/path/to/video.m3u8" type="application/x-mpegURL">
|
||||
<source src="http://your.static.provider.net/path/to/video.mp4" type="video/mp4">
|
||||
</video>
|
||||
```
|
||||
|
||||
The Flash-based HLS support will be tried before falling back to the MP4 source.
|
||||
|
||||
In "Source-first" mode, the tests run something like this:
|
||||
Can video.m3u8 play with Html5? No...
|
||||
Can video.m3u8 play with Flash? Yes! Use the first source.
|
||||
|
||||
## Flash Technology
|
||||
|
||||
The Flash playback tech is a part of the default `techOrder`. You may notice undesirable playback behavior in browsers that are subject to using this playback tech, in particular when scrubbing and seeking within a video. This behavior is a result of Flash's progressive video playback.
|
||||
|
||||
### Enabling Streaming Playback
|
||||
|
||||
In order to force the Flash tech to choose streaming playback, you need to provide a valid streaming source **before other valid Flash video sources**. This is necessary because of the source selection algorithm, where playback tech chooses the first possible source object with a valid type. Valid streaming `type` values include `rtmp/mp4` and `rtmp/flv`. The streaming `src` value requires valid connection and stream strings, separated by an `&`. An example of supplying a streaming source through your HTML markup might look like:
|
||||
|
||||
```html
|
||||
<source src="rtmp://your.streaming.provider.net/cfx/st/&mp4:path/to/video.mp4" type="rtmp/mp4">
|
||||
<source src="http://your.static.provider.net/path/to/video.mp4" type="video/mp4">
|
||||
<source src="http://your.static.provider.net/path/to/video.webm" type="video/webm">
|
||||
```
|
||||
|
||||
You may optionally use the last `/` as the separator between connection and stream strings, for example:
|
||||
|
||||
```html
|
||||
<source src="rtmp://your.streaming.provider.net/cfx/st/mp4:video.mp4" type="rtmp/mp4">
|
||||
```
|
||||
|
||||
All four RTMP protocols are valid in the `src` (RTMP, RTMPT, RTMPE, and RTMPS).
|
||||
|
||||
#### A note on sandboxing and security
|
||||
|
||||
In some environments, such as Electron and NW.js apps, stricter policies are enforced, and `.swf` files won’t be able to communicate with the outside world out of the box. To stream media, you have to add them to a special manifest of trusted files. [nw-flash-trust](https://github.com/szwacz/nw-flash-trust) makes this job easy.
|
||||
* [The Html5 Tech](https://github.com/videojs/video.js/blob/master/src/js/tech/html5.js)
|
||||
* [The videojs-flash project](http://github.com/videojs/videojs-flash/blob/master/src/index.js)
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
* [videojs()](#videojs)
|
||||
* [options](#options)
|
||||
* [getComponent()](#getcomponent)
|
||||
* [registerComponent](#registercomponent)
|
||||
* [registerComponent()](#registercomponent)
|
||||
* [getTech()](#gettech)
|
||||
* [registerTech](#registertech)
|
||||
* [registerTech()](#registertech)
|
||||
* [extend()](#extend)
|
||||
* [mergeOptions()](#mergeoptions)
|
||||
* [bind()](#bind)
|
||||
* [plugin()](#plugin)
|
||||
* [xhr](#xhr)
|
||||
* [xhr()](#xhr)
|
||||
|
||||
## `videojs()`
|
||||
|
||||
|
||||
+1093
-47
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+38
-1
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"Play": "Wiedergabe",
|
||||
"Pause": "Pause",
|
||||
"Replay": "Erneut abspielen",
|
||||
"Current Time": "Aktueller Zeitpunkt",
|
||||
"Duration Time": "Dauer",
|
||||
"Remaining Time": "Verbleibende Zeit",
|
||||
@@ -31,10 +32,46 @@
|
||||
", opens captions settings dialog": ", öffnet Einstellungen für Untertitel",
|
||||
", opens subtitles settings dialog": ", öffnet Einstellungen für Untertitel",
|
||||
", selected": ", ausgewählt",
|
||||
"captions settings": "Untertiteleinstellungen",
|
||||
"subititles settings": "Untertiteleinstellungen",
|
||||
"descriptions settings": "Einstellungen für Beschreibungen",
|
||||
"Close Modal Dialog": "Modales Fenster schließen",
|
||||
"Descriptions": "Beschreibungen",
|
||||
"descriptions off": "Beschreibungen aus",
|
||||
"The media is encrypted and we do not have the keys to decrypt it.": "Die Entschlüsselungsschlüssel für den verschlüsselten Medieninhalt sind nicht verfügbar.",
|
||||
", opens descriptions settings dialog": ", öffnet Einstellungen für Beschreibungen",
|
||||
"Audio Track": "Tonspur"
|
||||
"Audio Track": "Tonspur",
|
||||
"Text": "Schrift",
|
||||
"White": "Weiß",
|
||||
"Black": "Schwarz",
|
||||
"Red": "Rot",
|
||||
"Green": "Grün",
|
||||
"Blue": "Blau",
|
||||
"Yellow": "Gelb",
|
||||
"Magenta": "Magenta",
|
||||
"Cyan": "Türkis",
|
||||
"Background": "Hintergrund",
|
||||
"Window": "Fenster",
|
||||
"Transparent": "Durchsichtig",
|
||||
"Semi-Transparent": "Halbdurchsichtig",
|
||||
"Opaque": "Undurchsictig",
|
||||
"Font Size": "Schriftgröße",
|
||||
"Text Edge Style": "Textkantenstil",
|
||||
"None": "Kein",
|
||||
"Raised": "Erhoben",
|
||||
"Depressed": "Gedrückt",
|
||||
"Uniform": "Uniform",
|
||||
"Dropshadow": "Schlagschatten",
|
||||
"Font Family": "Schristfamilie",
|
||||
"Proportional Sans-Serif": "Proportionale Sans-Serif",
|
||||
"Monospace Sans-Serif": "Monospace Sans-Serif",
|
||||
"Proportional Serif": "Proportionale Serif",
|
||||
"Monospace Serif": "Monospace Serif",
|
||||
"Casual": "Zwanglos",
|
||||
"Script": "Schreibeschrift",
|
||||
"Small Caps": "Small-Caps",
|
||||
"Defaults": "Standardeinstellungen",
|
||||
"Done": "Fertig",
|
||||
"Caption Settings Dialog": "Einstellungsdialog für Untertitel",
|
||||
"Beginning of dialog window. Escape will cancel and close the window.": "Anfang des Dialogfensters. Esc bricht ab und schließt das Fenster."
|
||||
}
|
||||
|
||||
+38
-1
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"Play": "Play",
|
||||
"Pause": "Pause",
|
||||
"Replay": "Replay",
|
||||
"Current Time": "Current Time",
|
||||
"Duration Time": "Duration Time",
|
||||
"Remaining Time": "Remaining Time",
|
||||
@@ -36,5 +37,41 @@
|
||||
", opens captions settings dialog": ", opens captions settings dialog",
|
||||
", opens subtitles settings dialog": ", opens subtitles settings dialog",
|
||||
", opens descriptions settings dialog": ", opens descriptions settings dialog",
|
||||
", selected": ", selected"
|
||||
", selected": ", selected",
|
||||
"captions settings": "captions settings",
|
||||
"subititles settings": "subititles settings",
|
||||
"descriptions settings": "descriptions settings",
|
||||
"Text": "Text",
|
||||
"White": "White",
|
||||
"Black": "Black",
|
||||
"Red": "Red",
|
||||
"Green": "Green",
|
||||
"Blue": "Blue",
|
||||
"Yellow": "Yellow",
|
||||
"Magenta": "Magenta",
|
||||
"Cyan": "Cyan",
|
||||
"Background": "Background",
|
||||
"Window": "Window",
|
||||
"Transparent": "Transparent",
|
||||
"Semi-Transparent": "Semi-Transparent",
|
||||
"Opaque": "Opaque",
|
||||
"Font Size": "Font Size",
|
||||
"Text Edge Style": "Text Edge Style",
|
||||
"None": "None",
|
||||
"Raised": "Raised",
|
||||
"Depressed": "Depressed",
|
||||
"Uniform": "Uniform",
|
||||
"Dropshadow": "Dropshadow",
|
||||
"Font Family": "Font Family",
|
||||
"Proportional Sans-Serif": "Proportional Sans-Serif",
|
||||
"Monospace Sans-Serif": "Monospace Sans-Serif",
|
||||
"Proportional Serif": "Proportional Serif",
|
||||
"Monospace Serif": "Monospace Serif",
|
||||
"Casual": "Casual",
|
||||
"Script": "Script",
|
||||
"Small Caps": "Small Caps",
|
||||
"Defaults": "Defaults",
|
||||
"Done": "Done",
|
||||
"Caption Settings Dialog": "Caption Settings Dialog",
|
||||
"Beginning of dialog window. Escape will cancel and close the window.": "Beginning of dialog window. Escape will cancel and close the window."
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"Play": "Lecture",
|
||||
"Pause": "Pause",
|
||||
"Replay": "Revoir",
|
||||
"Current Time": "Temps actuel",
|
||||
"Duration Time": "Durée",
|
||||
"Remaining Time": "Temps restant",
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"Play": "Reproduzir",
|
||||
"Pause": "Parar",
|
||||
"Replay": "Reiniciar",
|
||||
"Current Time": "Tempo Atual",
|
||||
"Duration Time": "Duração",
|
||||
"Remaining Time": "Tempo Restante",
|
||||
"Stream Type": "Tipo de Stream",
|
||||
"LIVE": "EM DIRETO",
|
||||
"Loaded": "Carregado",
|
||||
"Progress": "Progresso",
|
||||
"Fullscreen": "Ecrã inteiro",
|
||||
"Non-Fullscreen": "Ecrã normal",
|
||||
"Mute": "Desativar som",
|
||||
"Unmute": "Ativar som",
|
||||
"Playback Rate": "Velocidade de reprodução",
|
||||
"Subtitles": "Legendas",
|
||||
"subtitles off": "desativar legendas",
|
||||
"Captions": "Anotações",
|
||||
"captions off": "desativar anotações",
|
||||
"Chapters": "Capítulos",
|
||||
"Close Modal Dialog": "Fechar Janela Modal",
|
||||
"Descriptions": "Descrições",
|
||||
"descriptions off": "desativar descrições",
|
||||
"Audio Track": "Faixa Áudio",
|
||||
"You aborted the media playback": "Parou a reprodução do vídeo.",
|
||||
"A network error caused the media download to fail part-way.": "Um erro na rede fez o vídeo falhar parcialmente.",
|
||||
"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 na rede ou no servidor, ou porque formato do vídeo não é compatível.",
|
||||
"The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "A reprodução foi interrompida por um problema com o vídeo ou porque o formato não é compatível com o seu navegador.",
|
||||
"No compatible source was found for this media.": "Não foi encontrada uma fonte de vídeo compatível.",
|
||||
"The media is encrypted and we do not have the keys to decrypt it.": "O vídeo está encriptado e não há uma chave para o desencriptar.",
|
||||
"Play Video": "Reproduzir Vídeo",
|
||||
"Close": "Fechar",
|
||||
"Modal Window": "Janela Modal",
|
||||
"This is a modal window": "Isto é uma janela modal",
|
||||
"This modal can be closed by pressing the Escape key or activating the close button.": "Esta modal pode ser fechada pressionando a tecla ESC ou ativando o botão de fechar.",
|
||||
", opens captions settings dialog": ", abre janela com definições de legendas",
|
||||
", opens subtitles settings dialog": ", abre janela com definições de legendas",
|
||||
", opens descriptions settings dialog": ", abre janela com definições de descrições",
|
||||
", selected": ", seleccionado"
|
||||
}
|
||||
+47
-4
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"Play": "Oynat",
|
||||
"Pause": "Duraklat",
|
||||
"Replay": "Yeniden Oynat",
|
||||
"Current Time": "Süre",
|
||||
"Duration Time": "Toplam Süre",
|
||||
"Remaining Time": "Kalan Süre",
|
||||
@@ -15,20 +16,62 @@
|
||||
"Playback Rate": "Oynatma Hızı",
|
||||
"Subtitles": "Altyazı",
|
||||
"subtitles off": "Altyazı Kapalı",
|
||||
"Captions": "Ek Açıklamalar",
|
||||
"captions off": "Ek Açıklamalar Kapalı",
|
||||
"Captions": "Altyazı",
|
||||
"captions off": "Altyazı Kapalı",
|
||||
"Chapters": "Bölümler",
|
||||
"Close Modal Dialog": "Dialogu Kapat",
|
||||
"Descriptions": "Açıklamalar",
|
||||
"descriptions off": "Açıklamalar kapalı",
|
||||
"Audio Track": "Ses Dosyası",
|
||||
"You aborted the media playback": "Video oynatmayı iptal ettiniz",
|
||||
"A network error caused the media download to fail part-way.": "Video indirilirken bağlantı sorunu oluştu.",
|
||||
"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.",
|
||||
"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.",
|
||||
"No compatible source was found for this media.": "Video için kaynak bulunamadı.",
|
||||
"The media is encrypted and we do not have the keys to decrypt it.": "Video, şifrelenmiş bir kaynaktan geliyor ve oynatmak için gerekli anahtar bulunamadı.",
|
||||
"Play Video": "Videoyu Oynat",
|
||||
"Close": "Kapat",
|
||||
"Modal Window": "Modal Penceresi",
|
||||
"This is a modal window": "Bu bir modal penceresidir",
|
||||
"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.",
|
||||
", opens captions settings dialog": ", ek açıklama ayarları menüsünü açar",
|
||||
", opens captions settings dialog": ", altyazı ayarları menüsünü açar",
|
||||
", opens subtitles settings dialog": ", altyazı ayarları menüsünü açar",
|
||||
", selected": ", seçildi"
|
||||
", opens descriptions settings dialog": ", açıklama ayarları menüsünü açar",
|
||||
", selected": ", seçildi",
|
||||
"captions settings": "altyazı ayarları",
|
||||
"subititles settings": "altyazı ayarları",
|
||||
"descriptions settings": "açıklama ayarları",
|
||||
"Text": "Yazı",
|
||||
"White": "Beyaz",
|
||||
"Black": "Siyah",
|
||||
"Red": "Kırmızı",
|
||||
"Green": "Yeşil",
|
||||
"Blue": "Mavi",
|
||||
"Yellow": "Sarı",
|
||||
"Magenta": "Macenta",
|
||||
"Cyan": "Açık Mavi (Camgöbeği)",
|
||||
"Background": "Arka plan",
|
||||
"Window": "Pencere",
|
||||
"Transparent": "Saydam",
|
||||
"Semi-Transparent": "Yarı-Saydam",
|
||||
"Opaque": "Mat",
|
||||
"Font Size": "Yazı Boyutu",
|
||||
"Text Edge Style": "Yazı Kenarlıkları",
|
||||
"None": "Hiçbiri",
|
||||
"Raised": "Kabartılmış",
|
||||
"Depressed": "Yassı",
|
||||
"Uniform": "Düz",
|
||||
"Dropshadow": "Gölgeli",
|
||||
"Font Family": "Yazı Tipi",
|
||||
"Proportional Sans-Serif": "Orantılı Sans-Serif",
|
||||
"Monospace Sans-Serif": "Eşaralıklı Sans-Serif",
|
||||
"Proportional Serif": "Orantılı Serif",
|
||||
"Monospace Serif": "Eşaralıklı Serif",
|
||||
"Casual": "Gündelik",
|
||||
"Script": "El Yazısı",
|
||||
"Small Caps": "Küçük Boyutlu Büyük Harfli",
|
||||
"Defaults": "Varsayılan Ayarlar",
|
||||
"Done": "Tamam",
|
||||
"Caption Settings Dialog": "Altyazı Ayarları Menüsü",
|
||||
"Beginning of dialog window. Escape will cancel and close the window.": "Diyalog penceresinin başlangıcı. ESC tuşu işlemi iptal edip pencereyi kapatacaktır."
|
||||
}
|
||||
|
||||
+21
-6
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"Play": "Phát",
|
||||
"Pause": "Tạm dừng",
|
||||
"Replay": "Phát lại",
|
||||
"Current Time": "Thời gian hiện tại",
|
||||
"Duration Time": "Độ dài",
|
||||
"Remaining Time": "Thời gian còn lại",
|
||||
@@ -12,15 +13,29 @@
|
||||
"Non-Fullscreen": "Thoát toàn màn hình",
|
||||
"Mute": "Tắt tiếng",
|
||||
"Unmute": "Bật âm thanh",
|
||||
"Playback Rate": "Tốc độ phát",
|
||||
"Playback Rate": "Tỉ lệ phát lại",
|
||||
"Subtitles": "Phụ đề",
|
||||
"subtitles off": "Tắt phụ đề",
|
||||
"subtitles off": "tắt phụ đề",
|
||||
"Captions": "Chú thích",
|
||||
"captions off": "Tắt chú thích",
|
||||
"captions off": "tắt chú thích",
|
||||
"Chapters": "Chương",
|
||||
"You aborted the media playback": "Bạn đã hủy việc phát media.",
|
||||
"Close Modal Dialog": "Đóng hộp thoại",
|
||||
"Descriptions": "Mô tả",
|
||||
"descriptions off": "tắt mô tả",
|
||||
"Audio Track": "Track âm thanh",
|
||||
"You aborted the media playback": "Bạn đã hủy việc phát lại media.",
|
||||
"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.",
|
||||
"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ợ.",
|
||||
"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ợ.",
|
||||
"No compatible source was found for this media.": "Không có nguồn tương thích cho media này."
|
||||
}
|
||||
"No compatible source was found for this media.": "Không có nguồn tương thích cho media này.",
|
||||
"The media is encrypted and we do not have the keys to decrypt it.": "Media đã được mã hóa và chúng tôi không thể giải mã nó.",
|
||||
"Play Video": "Phát Video",
|
||||
"Close": "Đóng",
|
||||
"Modal Window": "Cửa sổ",
|
||||
"This is a modal window": "Đây là một cửa sổ",
|
||||
"This modal can be closed by pressing the Escape key or activating the close button.": "Cửa sổ này có thể đóng lại bằng việc nhấn phím Esc hoặc kích hoạt nút đóng.",
|
||||
", opens captions settings dialog": ", mở hộp thoại thiết lập chú thích",
|
||||
", opens subtitles settings dialog": ", mở hộp thoại thiết lập phụ đề",
|
||||
", opens descriptions settings dialog": ", mở hộp thoại thiết lập mô tả",
|
||||
", selected": ", đã chọn"
|
||||
}
|
||||
+10
-8
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "video.js",
|
||||
"description": "An HTML5 and Flash video player with a common API and skin for both.",
|
||||
"version": "5.16.0",
|
||||
"version": "6.0.0-RC.1",
|
||||
"main": "./es5/video.js",
|
||||
"style": "./dist/video-js.css",
|
||||
"copyright": "Copyright Brightcove, Inc. <https://www.brightcove.com/>",
|
||||
@@ -25,10 +25,12 @@
|
||||
"start": "grunt dev",
|
||||
"test": "grunt test",
|
||||
"docs": "npm run docs:lint && npm run docs:api",
|
||||
"docs:api": "jsdoc -r src/js -d docs/api -c .jsdoc.json",
|
||||
"jsdoc": "jsdoc",
|
||||
"docs:api": "jsdoc -c .jsdoc.json",
|
||||
"docs:lint": "remark -- './**/*.md'",
|
||||
"docs:fix": "remark --output -- './**/*.md'",
|
||||
"babel": "babel src/js -d es5"
|
||||
"babel": "babel src/js -d es5",
|
||||
"prepublish": "not-in-install && npm run docs:api || in-install"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -41,18 +43,16 @@
|
||||
"tsml": "1.0.1",
|
||||
"videojs-font": "2.0.0",
|
||||
"videojs-ie8": "1.1.2",
|
||||
"videojs-swf": "5.1.0",
|
||||
"videojs-vtt.js": "0.12.1",
|
||||
"xhr": "2.2.2"
|
||||
"xhr": "2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aliasify": "^2.1.0",
|
||||
"babel-cli": "^6.11.4",
|
||||
"babel-plugin-inline-json": "^1.1.1",
|
||||
"babel-plugin-transform-es3-member-expression-literals": "^6.8.0",
|
||||
"babel-plugin-transform-es3-property-literals": "^6.8.0",
|
||||
"babel-plugin-transform-runtime": "^6.9.0",
|
||||
"babel-preset-es2015": "^6.14.0",
|
||||
"babel-preset-es3": "^1.0.1",
|
||||
"babel-register": "^6.9.0",
|
||||
"babelify": "^7.3.0",
|
||||
"blanket": "^1.1.6",
|
||||
@@ -89,9 +89,10 @@
|
||||
"grunt-version": "~1.1.1",
|
||||
"grunt-videojs-languages": "0.0.4",
|
||||
"grunt-zip": "0.17.1",
|
||||
"in-publish": "^2.0.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"jsdoc": "^3.4.2",
|
||||
"karma": "^1.2.0",
|
||||
"karma": "1.3.0",
|
||||
"karma-browserify": "^5.1.0",
|
||||
"karma-browserstack-launcher": "^1.0.1",
|
||||
"karma-chrome-launcher": "^2.0.0",
|
||||
@@ -116,6 +117,7 @@
|
||||
"shelljs": "^0.7.5",
|
||||
"sinon": "^1.16.1",
|
||||
"time-grunt": "^1.1.1",
|
||||
"tui-jsdoc-template": "^1.1.0",
|
||||
"uglify-js": "~2.7.3",
|
||||
"videojs-doc-generator": "0.0.1",
|
||||
"videojs-standard": "^6.0.1",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
@mixin transition($string: $transition--default) {
|
||||
-webkit-transition: $string;
|
||||
-moz-transition: $string;
|
||||
-ms-transition: $string;
|
||||
-o-transition: $string;
|
||||
transition: $string;
|
||||
}
|
||||
@@ -24,7 +25,6 @@
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
.vjs-current-time, .vjs-time-divider, .vjs-duration, .vjs-remaining-time,
|
||||
.vjs-playback-rate, .vjs-progress-control,
|
||||
.vjs-mute-control, .vjs-volume-control, .vjs-volume-menu-button,
|
||||
.vjs-mute-control, .vjs-volume-control,
|
||||
.vjs-chapters-button, .vjs-descriptions-button, .vjs-captions-button,
|
||||
.vjs-subtitles-button, .vjs-audio-button { display: none; }
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
.video-js.vjs-layout-x-small:not(.vjs-fullscreen) {
|
||||
.vjs-current-time, .vjs-time-divider, .vjs-duration, .vjs-remaining-time,
|
||||
.vjs-playback-rate,
|
||||
.vjs-mute-control, .vjs-volume-control, .vjs-volume-menu-button,
|
||||
.vjs-mute-control, .vjs-volume-control,
|
||||
.vjs-chapters-button, .vjs-descriptions-button, .vjs-captions-button,
|
||||
.vjs-subtitles-button, .vjs-audio-button { display: none; }
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
|
||||
.video-js:hover .vjs-big-play-button,
|
||||
.video-js .vjs-big-play-button:focus {
|
||||
outline: 0;
|
||||
border-color: $primary-foreground-color;
|
||||
|
||||
@include background-color-with-alpha($secondary-background-color, $secondary-background-transparency);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// It's used on both real buttons (play button)
|
||||
// and div buttons (menu buttons)
|
||||
.video-js .vjs-control {
|
||||
outline: none;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
|
||||
@@ -6,3 +6,6 @@
|
||||
.video-js .vjs-play-control.vjs-playing {
|
||||
@extend .vjs-icon-pause;
|
||||
}
|
||||
.video-js .vjs-play-control.vjs-ended {
|
||||
@extend .vjs-icon-replay;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
// .vjs-progress-control / ProgressControl
|
||||
//
|
||||
// Let's talk pixel math!
|
||||
// Start with a base font size of 10px (assuming that hasn't changed)
|
||||
// No Hover:
|
||||
// - Progress holder is 3px
|
||||
// - Progress handle is 9px
|
||||
// - Progress handle is pulled up 3px to center it.
|
||||
//
|
||||
// Hover:
|
||||
// - Progress holder becomes 5px
|
||||
// - Progress handle becomes 15px
|
||||
// - Progress handle is pulled up 5px to center it
|
||||
//
|
||||
|
||||
// This is the container for all progress bar-related components/elements.
|
||||
.video-js .vjs-progress-control {
|
||||
@include flex(auto);
|
||||
@include display-flex(center);
|
||||
@@ -22,35 +11,32 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Box containing play and load progresses. Also acts as seek scrubber.
|
||||
.vjs-no-flex .vjs-progress-control {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// .vjs-progress-holder / SeekBar
|
||||
//
|
||||
// Box containing play and load progress bars. It also acts as seek scrubber.
|
||||
.video-js .vjs-progress-holder {
|
||||
@include flex(auto);
|
||||
@include transition(all 0.2s);
|
||||
height: 0.3em;
|
||||
}
|
||||
|
||||
// We need an increased hit area on hover
|
||||
// This increases the size of the progress holder so there is an increased
|
||||
// hit area for clicks/touches.
|
||||
.video-js .vjs-progress-control:hover .vjs-progress-holder {
|
||||
font-size: 1.666666666666666666em;
|
||||
}
|
||||
|
||||
/* If we let the font size grow as much as everything else, the current time tooltip ends up
|
||||
ginormous. If you'd like to enable the current time tooltip all the time, this should be disabled
|
||||
to avoid a weird hitch when you roll off the hover. */
|
||||
|
||||
// Also show the current time tooltip
|
||||
.video-js .vjs-progress-control:hover .vjs-time-tooltip,
|
||||
.video-js .vjs-progress-control:hover .vjs-mouse-display:after,
|
||||
.video-js .vjs-progress-control:hover .vjs-play-progress:after {
|
||||
font-family: $text-font-family;
|
||||
visibility: visible;
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
// Progress Bars
|
||||
// .vjs-play-progress / PlayProgressBar and .vjs-load-progress / LoadProgressBar
|
||||
//
|
||||
// These are bars that appear within the progress control to communicate the
|
||||
// amount of media that has played back and the amount of media that has
|
||||
// loaded, respectively.
|
||||
.video-js .vjs-progress-holder .vjs-play-progress,
|
||||
.video-js .vjs-progress-holder .vjs-load-progress,
|
||||
.video-js .vjs-progress-holder .vjs-tooltip-progress-bar,
|
||||
.video-js .vjs-progress-holder .vjs-load-progress div {
|
||||
position: absolute;
|
||||
display: block;
|
||||
@@ -64,86 +50,80 @@
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.video-js .vjs-mouse-display {
|
||||
@extend .vjs-icon-circle;
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.video-js .vjs-play-progress {
|
||||
background-color: $primary-foreground-color;
|
||||
@extend .vjs-icon-circle;
|
||||
|
||||
// Progress handle
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: -0.333333333333333em;
|
||||
right: -0.5em;
|
||||
font-size: 0.9em;
|
||||
position: absolute;
|
||||
right: -0.5em;
|
||||
top: -0.333333333333333em;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Current Time "tooltip"
|
||||
// By default this is hidden and only shown when hovering over the progress control
|
||||
.video-js .vjs-time-tooltip,
|
||||
.video-js .vjs-mouse-display:after,
|
||||
.video-js .vjs-play-progress:after {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: -3.4em;
|
||||
right: -1.9em;
|
||||
font-size: 0.9em;
|
||||
color: #000;
|
||||
content: attr(data-current-time);
|
||||
padding: 6px 8px 8px 8px;
|
||||
@include background-color-with-alpha(#fff, 0.8);
|
||||
@include border-radius(0.3em);
|
||||
}
|
||||
|
||||
.video-js .vjs-time-tooltip,
|
||||
.video-js .vjs-play-progress:before,
|
||||
.video-js .vjs-play-progress:after {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.video-js .vjs-progress-control .vjs-keep-tooltips-inside:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.video-js .vjs-load-progress {
|
||||
// For IE8 we'll lighten the color
|
||||
// For IE8, we'll lighten the color
|
||||
background: lighten($secondary-background-color, 25%);
|
||||
// Otherwise we'll rely on stacked opacities
|
||||
// Otherwise, we'll rely on stacked opacities
|
||||
background: rgba($secondary-background-color, $secondary-background-transparency);
|
||||
}
|
||||
|
||||
// there are child elements of the load progress bar that represent the
|
||||
// specific time ranges that have been buffered
|
||||
// There are child elements of the load progress bar that represent the
|
||||
// specific time ranges that have been buffered.
|
||||
.video-js .vjs-load-progress div {
|
||||
// For IE8 we'll lighten the color
|
||||
// For IE8, we'll lighten the color
|
||||
background: lighten($secondary-background-color, 50%);
|
||||
// Otherwise we'll rely on stacked opacities
|
||||
// Otherwise, we'll rely on stacked opacities
|
||||
background: rgba($secondary-background-color, 0.75);
|
||||
}
|
||||
|
||||
.video-js.vjs-no-flex .vjs-progress-control {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// .vjs-time-tooltip
|
||||
//
|
||||
// These elements are displayed above the progress bar. They are not components
|
||||
// themselves, but they are managed by the MouseTimeDisplay and PlayProgressBar
|
||||
// components individually.
|
||||
//
|
||||
// By default, they are hidden and only shown when hovering over the progress
|
||||
// control.
|
||||
.video-js .vjs-time-tooltip {
|
||||
@include background-color-with-alpha(#fff, 0.8);
|
||||
@include border-radius(0.3em);
|
||||
color: #000;
|
||||
display: inline-block;
|
||||
height: 2.4em;
|
||||
position: relative;
|
||||
|
||||
// By floating the tooltips to the right, their right edge becomes aligned
|
||||
// with the right edge of their parent element. However, in order to have them
|
||||
// centered, they must be pulled further to the right via positioning (e.g.
|
||||
// `right: -10px;`. This part is left to JavaScript.
|
||||
float: right;
|
||||
right: -1.9em;
|
||||
}
|
||||
font-family: $text-font-family;
|
||||
|
||||
.vjs-tooltip-progress-bar {
|
||||
// The font-size should translate to a consistent 10px for time tooltips in
|
||||
// all states. This is tricky because the .vjs-progress-holder element
|
||||
// changes its font-size when the .vjs-progress-control is hovered.
|
||||
font-size: 1em;
|
||||
padding: 6px 8px 8px 8px;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
top: -3.4em;
|
||||
visibility: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.video-js .vjs-progress-control:hover .vjs-time-tooltip {
|
||||
|
||||
// Ensure that we maintain a font-size of ~10px.
|
||||
font-size: 0.6em;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
// .vjs-mouse-display / MouseTimeDisplay
|
||||
//
|
||||
// This element tracks the mouse position along the progress control and
|
||||
// includes a tooltip, which displays the time at that point in the media.
|
||||
.video-js .vjs-progress-control .vjs-mouse-display {
|
||||
display: none;
|
||||
position: absolute;
|
||||
@@ -152,25 +132,27 @@
|
||||
background-color: #000;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.vjs-no-flex .vjs-progress-control .vjs-mouse-display {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.video-js .vjs-progress-control:hover .vjs-mouse-display {
|
||||
display: block;
|
||||
}
|
||||
.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display,
|
||||
.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display:after {
|
||||
|
||||
.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
$trans: visibility 1.0s, opacity 1.0s;
|
||||
@include transition($trans);
|
||||
}
|
||||
.video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display,
|
||||
.video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display:after {
|
||||
|
||||
.video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display {
|
||||
display: none;
|
||||
}
|
||||
.vjs-mouse-display .vjs-time-tooltip,
|
||||
.video-js .vjs-progress-control .vjs-mouse-display:after {
|
||||
|
||||
.vjs-mouse-display .vjs-time-tooltip {
|
||||
color: #fff;
|
||||
@include background-color-with-alpha(#000, 0.8);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.video-js .vjs-slider {
|
||||
outline: 0;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
// IE 8 + IE 9 width: auto container fix
|
||||
.vjs-no-flex .vjs-remaining-time.vjs-time-control.vjs-control {
|
||||
width: 0px !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.video-js .vjs-duration,
|
||||
.vjs-no-flex .vjs-duration {
|
||||
display: none;
|
||||
|
||||
@@ -1,27 +1,128 @@
|
||||
.video-js .vjs-mute-control,
|
||||
.video-js .vjs-volume-menu-button {
|
||||
.video-js .vjs-mute-control {
|
||||
cursor: pointer;
|
||||
@include flex(none);
|
||||
@extend .vjs-icon-volume-high;
|
||||
// padding here is for IE < 9, it doesn't do width: auto from
|
||||
// another style correctly
|
||||
padding-left: 2em;
|
||||
padding-right: 2em;
|
||||
padding-bottom: 3em;
|
||||
}
|
||||
|
||||
.video-js .vjs-mute-control.vjs-vol-0,
|
||||
.video-js .vjs-volume-menu-button.vjs-vol-0 {
|
||||
.video-js .vjs-mute-control.vjs-vol-0 {
|
||||
@extend .vjs-icon-volume-mute;
|
||||
}
|
||||
.video-js .vjs-mute-control.vjs-vol-1,
|
||||
.video-js .vjs-volume-menu-button.vjs-vol-1 {
|
||||
.video-js .vjs-mute-control.vjs-vol-1 {
|
||||
@extend .vjs-icon-volume-low;
|
||||
}
|
||||
.video-js .vjs-mute-control.vjs-vol-2,
|
||||
.video-js .vjs-volume-menu-button.vjs-vol-2 {
|
||||
.video-js .vjs-mute-control.vjs-vol-2 {
|
||||
@extend .vjs-icon-volume-mid;
|
||||
}
|
||||
|
||||
.video-js .vjs-volume-control {
|
||||
cursor: pointer;
|
||||
margin-right: 1em;
|
||||
@include display-flex;
|
||||
}
|
||||
.video-js .vjs-volume-control.vjs-volume-horizontal {
|
||||
width: 5em;
|
||||
@include flex(none);
|
||||
@include display-flex(center);
|
||||
}
|
||||
|
||||
.video-js .vjs-volume-panel .vjs-volume-control {
|
||||
visibility: visible;
|
||||
opacity: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin-left: -1px;
|
||||
|
||||
}
|
||||
.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
|
||||
& .vjs-volume-bar,
|
||||
& .vjs-volume-level {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
|
||||
}
|
||||
}
|
||||
|
||||
.video-js .vjs-volume-panel {
|
||||
&:hover .vjs-volume-control,
|
||||
&:active .vjs-volume-control,
|
||||
&:focus .vjs-volume-control,
|
||||
& .vjs-volume-control:hover ,
|
||||
& .vjs-volume-control:active ,
|
||||
& .vjs-volume-control:focus ,
|
||||
& .vjs-mute-control:hover ~ .vjs-volume-control,
|
||||
& .vjs-mute-control:active ~ .vjs-volume-control,
|
||||
& .vjs-mute-control:focus ~ .vjs-volume-control,
|
||||
& .vjs-volume-control.vjs-slider-active {
|
||||
&.vjs-volume-horizontal {
|
||||
width: 5em;
|
||||
height: 3em;
|
||||
}
|
||||
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
|
||||
&.vjs-volume-vertical {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
|
||||
& .vjs-volume-bar,
|
||||
& .vjs-volume-level {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
|
||||
}
|
||||
}
|
||||
|
||||
$transition-property: visibility 0.1s, opacity 0.1s, height 0.1s, width 0.1s, left 0s, top 0s;
|
||||
@include transition($transition-property);
|
||||
}
|
||||
|
||||
&.vjs-volume-panel-horizontal {
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active,
|
||||
&.vjs-slider-active {
|
||||
width: 9em;
|
||||
|
||||
@include transition(width 0.1s);
|
||||
}
|
||||
}
|
||||
|
||||
@include transition(width 1s);
|
||||
}
|
||||
|
||||
.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical {
|
||||
height: 8em;
|
||||
width: 3em;
|
||||
left: -3.5em;
|
||||
|
||||
$transition-property: visibility 1s, opacity 1s, height 1s 1s, width 1s 1s, left 1s 1s, top 1s 1s;
|
||||
@include transition($transition-property)
|
||||
}
|
||||
.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal {
|
||||
$transition-property: visibility 1s, opacity 1s, height 1s 1s, width 1s, left 1s 1s, top 1s 1s;
|
||||
@include transition($transition-property)
|
||||
}
|
||||
|
||||
.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal {
|
||||
width: 5em;
|
||||
height: 3em;
|
||||
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
|
||||
@include transition(none);
|
||||
}
|
||||
|
||||
.video-js.vjs-no-flex .vjs-volume-control.vjs-volume-vertical,
|
||||
.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical {
|
||||
position: absolute;
|
||||
bottom: 3em;
|
||||
left: 0.5em;
|
||||
}
|
||||
|
||||
.video-js .vjs-volume-panel {
|
||||
@include display-flex;
|
||||
}
|
||||
|
||||
.video-js .vjs-volume-bar {
|
||||
@@ -74,6 +175,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.video-js .vjs-volume-panel.vjs-volume-panel-vertical {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
// Assumes volume starts at 1.0.
|
||||
.vjs-volume-bar.vjs-slider-vertical .vjs-volume-level {
|
||||
height: 100%;
|
||||
@@ -83,53 +188,14 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// The volume menu button is like menu buttons (captions/subtitles) but works
|
||||
// a little differently. It needs to be possible to tab to the volume slider
|
||||
// without hitting space bar on the menu button. To do this we're not using
|
||||
// display:none to hide the slider menu by default, and instead setting the
|
||||
// width and height to zero.
|
||||
.vjs-menu-button-popup.vjs-volume-menu-button .vjs-menu {
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top-color: transparent;
|
||||
.video-js .vjs-volume-vertical {
|
||||
width: 3em;
|
||||
height: 8em;
|
||||
bottom: 8em;
|
||||
|
||||
@include background-color-with-alpha($primary-background-color, $primary-background-transparency);
|
||||
}
|
||||
|
||||
.vjs-menu-button-popup.vjs-volume-menu-button-vertical .vjs-menu {
|
||||
left: 0.5em;
|
||||
height: 8em;
|
||||
}
|
||||
.vjs-menu-button-popup.vjs-volume-menu-button-horizontal .vjs-menu {
|
||||
.video-js .vjs-volume-horizontal .vjs-menu {
|
||||
left: -2em;
|
||||
}
|
||||
|
||||
.vjs-menu-button-popup.vjs-volume-menu-button .vjs-menu-content {
|
||||
height: 0;
|
||||
width: 0;
|
||||
|
||||
// Avoids unnecessary scrollbars in the menu content. Primarily noticed in Chrome on Linux.
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.vjs-volume-menu-button-vertical:hover .vjs-menu-content,
|
||||
.vjs-volume-menu-button-vertical:focus .vjs-menu-content,
|
||||
.vjs-volume-menu-button-vertical.vjs-slider-active .vjs-menu-content,
|
||||
.vjs-volume-menu-button-vertical .vjs-lock-showing .vjs-menu-content {
|
||||
height: 8em;
|
||||
width: 2.9em;
|
||||
}
|
||||
|
||||
.vjs-volume-menu-button-horizontal:hover .vjs-menu-content,
|
||||
.vjs-volume-menu-button-horizontal:focus .vjs-menu-content,
|
||||
.vjs-volume-menu-button-horizontal .vjs-slider-active .vjs-menu-content,
|
||||
.vjs-volume-menu-button-horizontal .vjs-lock-showing .vjs-menu-content {
|
||||
height: 2.9em;
|
||||
width: 8em;
|
||||
}
|
||||
|
||||
.vjs-volume-menu-button.vjs-menu-button-inline .vjs-menu-content {
|
||||
// An inline volume should never have a menu background color.
|
||||
// This protects it from external changes to background colors.
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
@@ -18,11 +18,6 @@
|
||||
// This width is currently specific to the inline volume bar.
|
||||
width: 12em;
|
||||
}
|
||||
// Don't transition when tabbing in reverse to the volume menu
|
||||
// because it looks weird
|
||||
.video-js .vjs-menu-button-inline.vjs-slider-active {
|
||||
@include transition(none);
|
||||
}
|
||||
|
||||
.vjs-menu-button-inline .vjs-menu {
|
||||
opacity: 0;
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
|
||||
.vjs-menu li.vjs-menu-item:focus,
|
||||
.vjs-menu li.vjs-menu-item:hover {
|
||||
outline: 0;
|
||||
@include background-color-with-alpha($secondary-background-color, $secondary-background-transparency);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@import "private-variables";
|
||||
@import "utilities";
|
||||
|
||||
@import "node_modules/videojs-font/scss/icons";
|
||||
@import "../../node_modules/videojs-font/scss/icons";
|
||||
|
||||
@import "components/layout";
|
||||
@import "components/big-play";
|
||||
|
||||
+6
-17
@@ -16,8 +16,9 @@ class Button extends ClickableComponent {
|
||||
/**
|
||||
* Create the `Button`s DOM element.
|
||||
*
|
||||
* @param {string} [tag=button]
|
||||
* Element's node type. e.g. 'button'
|
||||
* @param {string} [tag="button"]
|
||||
* The element's node type. This argument is IGNORED: no matter what
|
||||
* is passed, it will always create a `button` element.
|
||||
*
|
||||
* @param {Object} [props={}]
|
||||
* An object of properties that should be set on the element.
|
||||
@@ -28,25 +29,13 @@ class Button extends ClickableComponent {
|
||||
* @return {Element}
|
||||
* The element that gets created.
|
||||
*/
|
||||
createEl(tag = 'button', props = {}, attributes = {}) {
|
||||
createEl(tag, props = {}, attributes = {}) {
|
||||
tag = 'button';
|
||||
|
||||
props = assign({
|
||||
className: this.buildCSSClass()
|
||||
}, props);
|
||||
|
||||
if (tag !== 'button') {
|
||||
log.warn(`Creating a Button with an HTML element of ${tag} is deprecated; use ClickableComponent instead.`);
|
||||
|
||||
// Add properties for clickable element which is not a native HTML button
|
||||
props = assign({
|
||||
tabIndex: 0
|
||||
}, props);
|
||||
|
||||
// Add ARIA attributes for clickable element which is not a native HTML button
|
||||
attributes = assign({
|
||||
role: 'button'
|
||||
}, attributes);
|
||||
}
|
||||
|
||||
// Add attributes for button element
|
||||
attributes = assign({
|
||||
|
||||
|
||||
@@ -108,9 +108,8 @@ class ClickableComponent extends Component {
|
||||
* @param {Element} [el=this.el()]
|
||||
* Element to set the title on.
|
||||
*
|
||||
* @return {string|ClickableComponent}
|
||||
* @return {string}
|
||||
* - The control text when getting
|
||||
* - Returns itself when setting; method can be chained.
|
||||
*/
|
||||
controlText(text, el = this.el()) {
|
||||
if (!text) {
|
||||
@@ -122,8 +121,6 @@ class ClickableComponent extends Component {
|
||||
this.controlText_ = text;
|
||||
this.controlTextEl_.innerHTML = localizedText;
|
||||
el.setAttribute('title', localizedText);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,9 +135,6 @@ class ClickableComponent extends Component {
|
||||
|
||||
/**
|
||||
* Enable this `Component`s element.
|
||||
*
|
||||
* @return {ClickableComponent}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
enable() {
|
||||
this.removeClass('vjs-disabled');
|
||||
@@ -152,14 +146,10 @@ class ClickableComponent extends Component {
|
||||
this.on('click', this.handleClick);
|
||||
this.on('focus', this.handleFocus);
|
||||
this.on('blur', this.handleBlur);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable this `Component`s element.
|
||||
*
|
||||
* @return {ClickableComponent}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
disable() {
|
||||
this.addClass('vjs-disabled');
|
||||
@@ -171,7 +161,6 @@ class ClickableComponent extends Component {
|
||||
this.off('click', this.handleClick);
|
||||
this.off('focus', this.handleFocus);
|
||||
this.off('blur', this.handleBlur);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+146
-333
@@ -4,10 +4,12 @@
|
||||
* @file component.js
|
||||
*/
|
||||
import window from 'global/window';
|
||||
import evented from './mixins/evented';
|
||||
import stateful from './mixins/stateful';
|
||||
import * as Dom from './utils/dom.js';
|
||||
import * as DomData from './utils/dom-data';
|
||||
import * as Fn from './utils/fn.js';
|
||||
import * as Guid from './utils/guid.js';
|
||||
import * as Events from './utils/events.js';
|
||||
import log from './utils/log.js';
|
||||
import toTitleCase from './utils/to-title-case.js';
|
||||
import mergeOptions from './utils/merge-options.js';
|
||||
@@ -82,6 +84,10 @@ class Component {
|
||||
this.el_ = this.createEl();
|
||||
}
|
||||
|
||||
// Make this an evented object and use `el_`, if available, as its event bus
|
||||
evented(this, {eventBusKey: this.el_ ? 'el_' : null});
|
||||
stateful(this, this.constructor.defaultState);
|
||||
|
||||
this.children_ = [];
|
||||
this.childIndex_ = {};
|
||||
this.childNameIndex_ = {};
|
||||
@@ -133,15 +139,12 @@ class Component {
|
||||
this.childIndex_ = null;
|
||||
this.childNameIndex_ = null;
|
||||
|
||||
// Remove all event listeners.
|
||||
this.off();
|
||||
|
||||
// Remove element from DOM
|
||||
if (this.el_.parentNode) {
|
||||
this.el_.parentNode.removeChild(this.el_);
|
||||
}
|
||||
|
||||
Dom.removeElData(this.el_);
|
||||
DomData.removeData(this.el_);
|
||||
this.el_ = null;
|
||||
}
|
||||
|
||||
@@ -341,21 +344,6 @@ class Component {
|
||||
if (typeof child === 'string') {
|
||||
componentName = toTitleCase(child);
|
||||
|
||||
// Options can also be specified as a boolean,
|
||||
// so convert to an empty object if false.
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
// Same as above, but true is deprecated so show a warning.
|
||||
if (options === true) {
|
||||
log.warn('Initializing a child component with `true` is deprecated.' +
|
||||
'Children should be defined in an array when possible, ' +
|
||||
'but if necessary use an object instead of `true`.'
|
||||
);
|
||||
options = {};
|
||||
}
|
||||
|
||||
const componentClassName = options.componentClass || componentName;
|
||||
|
||||
// Set name through options
|
||||
@@ -564,188 +552,9 @@ class Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an `event listener` to this `Component`s element.
|
||||
*
|
||||
* The benefit of using this over the following:
|
||||
* - `VjsEvents.on(otherElement, 'eventName', myFunc)`
|
||||
* - `otherComponent.on('eventName', myFunc)`
|
||||
*
|
||||
* 1. Is that the listeners will get cleaned up when either component gets disposed.
|
||||
* 1. It will also bind `myComponent` as the context of `myFunc`.
|
||||
* > NOTE: If you remove the element from the DOM that has used `on` you need to
|
||||
* clean up references using: `myComponent.trigger(el, 'dispose')`
|
||||
* This will also allow the browser to garbage collect it. In special
|
||||
* cases such as with `window` and `document`, which are both permanent,
|
||||
* this is not necessary.
|
||||
*
|
||||
* @param {string|Component|string[]} [first]
|
||||
* The event name, and array of event names, or another `Component`.
|
||||
*
|
||||
* @param {EventTarget~EventListener|string|string[]} [second]
|
||||
* The listener function, an event name, or an Array of events names.
|
||||
*
|
||||
* @param {EventTarget~EventListener} [third]
|
||||
* The event handler if `first` is a `Component` and `second` is an event name
|
||||
* or an Array of event names.
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
*
|
||||
* @listens Component#dispose
|
||||
*/
|
||||
on(first, second, third) {
|
||||
if (typeof first === 'string' || Array.isArray(first)) {
|
||||
Events.on(this.el_, first, Fn.bind(this, second));
|
||||
|
||||
// Targeting another component or element
|
||||
} else {
|
||||
const target = first;
|
||||
const type = second;
|
||||
const fn = Fn.bind(this, third);
|
||||
|
||||
// When this component is disposed, remove the listener from the other component
|
||||
const removeOnDispose = () => this.off(target, type, fn);
|
||||
|
||||
// Use the same function ID so we can remove it later it using the ID
|
||||
// of the original listener
|
||||
removeOnDispose.guid = fn.guid;
|
||||
this.on('dispose', removeOnDispose);
|
||||
|
||||
// If the other component is disposed first we need to clean the reference
|
||||
// to the other component in this component's removeOnDispose listener
|
||||
// Otherwise we create a memory leak.
|
||||
const cleanRemover = () => this.off('dispose', removeOnDispose);
|
||||
|
||||
// Add the same function ID so we can easily remove it later
|
||||
cleanRemover.guid = fn.guid;
|
||||
|
||||
// Check if this is a DOM node
|
||||
if (first.nodeName) {
|
||||
// Add the listener to the other element
|
||||
Events.on(target, type, fn);
|
||||
Events.on(target, 'dispose', cleanRemover);
|
||||
|
||||
// Should be a component
|
||||
// Not using `instanceof Component` because it makes mock players difficult
|
||||
} else if (typeof first.on === 'function') {
|
||||
// Add the listener to the other component
|
||||
target.on(type, fn);
|
||||
target.on('dispose', cleanRemover);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an event listener from this `Component`s element. If the second argument is
|
||||
* exluded all listeners for the type passed in as the first argument will be removed.
|
||||
*
|
||||
* @param {string|Component|string[]} [first]
|
||||
* The event name, and array of event names, or another `Component`.
|
||||
*
|
||||
* @param {EventTarget~EventListener|string|string[]} [second]
|
||||
* The listener function, an event name, or an Array of events names.
|
||||
*
|
||||
* @param {EventTarget~EventListener} [third]
|
||||
* The event handler if `first` is a `Component` and `second` is an event name
|
||||
* or an Array of event names.
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
off(first, second, third) {
|
||||
if (!first || typeof first === 'string' || Array.isArray(first)) {
|
||||
Events.off(this.el_, first, second);
|
||||
} else {
|
||||
const target = first;
|
||||
const type = second;
|
||||
// Ensure there's at least a guid, even if the function hasn't been used
|
||||
const fn = Fn.bind(this, third);
|
||||
|
||||
// Remove the dispose listener on this component,
|
||||
// which was given the same guid as the event listener
|
||||
this.off('dispose', fn);
|
||||
|
||||
if (first.nodeName) {
|
||||
// Remove the listener
|
||||
Events.off(target, type, fn);
|
||||
// Remove the listener for cleaning the dispose listener
|
||||
Events.off(target, 'dispose', fn);
|
||||
} else {
|
||||
target.off(type, fn);
|
||||
target.off('dispose', fn);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event listener that gets triggered only once and then gets removed.
|
||||
*
|
||||
* @param {string|Component|string[]} [first]
|
||||
* The event name, and array of event names, or another `Component`.
|
||||
*
|
||||
* @param {EventTarget~EventListener|string|string[]} [second]
|
||||
* The listener function, an event name, or an Array of events names.
|
||||
*
|
||||
* @param {EventTarget~EventListener} [third]
|
||||
* The event handler if `first` is a `Component` and `second` is an event name
|
||||
* or an Array of event names.
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
one(first, second, third) {
|
||||
if (typeof first === 'string' || Array.isArray(first)) {
|
||||
Events.one(this.el_, first, Fn.bind(this, second));
|
||||
} else {
|
||||
const target = first;
|
||||
const type = second;
|
||||
const fn = Fn.bind(this, third);
|
||||
|
||||
const newFunc = () => {
|
||||
this.off(target, type, newFunc);
|
||||
fn.apply(null, arguments);
|
||||
};
|
||||
|
||||
// Keep the same function ID so we can remove it later
|
||||
newFunc.guid = fn.guid;
|
||||
|
||||
this.on(target, type, newFunc);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger an event on an element.
|
||||
*
|
||||
* @param {EventTarget~Event|Object|string} event
|
||||
* The event name, and Event, or an event-like object with a type attribute
|
||||
* set to the event name.
|
||||
*
|
||||
* @param {Object} [hash]
|
||||
* Data hash to pass along with the event
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
trigger(event, hash) {
|
||||
Events.trigger(this.el_, event, hash);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a listener to the component's ready state. If the ready event has already
|
||||
* happened it will trigger the function immediately.
|
||||
*
|
||||
* @param {Component~ReadyCallback} fn
|
||||
* A function to call when ready is triggered.
|
||||
*
|
||||
* @param {boolean} [sync=false]
|
||||
* Execute the listener synchronously if `Component` is ready.
|
||||
* Bind a listener to the component's ready state.
|
||||
* Different from event listeners in that if the ready event has already happened
|
||||
* it will trigger the function immediately.
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
@@ -764,7 +573,6 @@ class Component {
|
||||
this.readyQueue_.push(fn);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -854,7 +662,7 @@ class Component {
|
||||
* - False if the `Component` does not have the class`
|
||||
*/
|
||||
hasClass(classToCheck) {
|
||||
return Dom.hasElClass(this.el_, classToCheck);
|
||||
return Dom.hasClass(this.el_, classToCheck);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -862,13 +670,9 @@ class Component {
|
||||
*
|
||||
* @param {string} classToAdd
|
||||
* CSS class name to add
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
addClass(classToAdd) {
|
||||
Dom.addElClass(this.el_, classToAdd);
|
||||
return this;
|
||||
Dom.addClass(this.el_, classToAdd);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -876,13 +680,9 @@ class Component {
|
||||
*
|
||||
* @param {string} classToRemove
|
||||
* CSS class name to remove
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
removeClass(classToRemove) {
|
||||
Dom.removeElClass(this.el_, classToRemove);
|
||||
return this;
|
||||
Dom.removeClass(this.el_, classToRemove);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -895,65 +695,45 @@ class Component {
|
||||
*
|
||||
* @param {boolean|Dom~predicate} [predicate]
|
||||
* An {@link Dom~predicate} function or a boolean
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
toggleClass(classToToggle, predicate) {
|
||||
Dom.toggleElClass(this.el_, classToToggle, predicate);
|
||||
return this;
|
||||
Dom.toggleClass(this.el_, classToToggle, predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the `Component`s element if it is hidden by removing the
|
||||
* 'vjs-hidden' class name from it.
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
show() {
|
||||
this.removeClass('vjs-hidden');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the `Component`s element if it is currently showing by adding the
|
||||
* 'vjs-hidden` class name to it.
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
hide() {
|
||||
this.addClass('vjs-hidden');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing'
|
||||
* class name to it. Used during fadeIn/fadeOut.
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
lockShowing() {
|
||||
this.addClass('vjs-lock-showing');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing'
|
||||
* class name from it. Used during fadeIn/fadeOut.
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
unlockShowing() {
|
||||
this.removeClass('vjs-lock-showing');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -984,14 +764,10 @@ class Component {
|
||||
* @param {string} value
|
||||
* Value to set the attribute to.
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
*
|
||||
* @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
|
||||
*/
|
||||
setAttribute(attribute, value) {
|
||||
Dom.setAttribute(this.el_, attribute, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1000,14 +776,10 @@ class Component {
|
||||
* @param {string} attribute
|
||||
* Name of the attribute to remove.
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
*
|
||||
* @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
|
||||
*/
|
||||
removeAttribute(attribute) {
|
||||
Dom.removeAttribute(this.el_, attribute);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1020,10 +792,9 @@ class Component {
|
||||
* @param {boolean} [skipListeners]
|
||||
* Skip the resize event trigger
|
||||
*
|
||||
* @return {Component|number|string}
|
||||
* - The width when getting, zero if there is no width. Can be a string
|
||||
* @return {number|string}
|
||||
* The width when getting, zero if there is no width. Can be a string
|
||||
* postpixed with '%' or 'px'.
|
||||
* - Returns itself when setting; method can be chained.
|
||||
*/
|
||||
width(num, skipListeners) {
|
||||
return this.dimension('width', num, skipListeners);
|
||||
@@ -1039,10 +810,9 @@ class Component {
|
||||
* @param {boolean} [skipListeners]
|
||||
* Skip the resize event trigger
|
||||
*
|
||||
* @return {Component|number|string}
|
||||
* - The width when getting, zero if there is no width. Can be a string
|
||||
* postpixed with '%' or 'px'.
|
||||
* - Returns itself when setting; method can be chained.
|
||||
* @return {number|string}
|
||||
* The width when getting, zero if there is no width. Can be a string
|
||||
* postpixed with '%' or 'px'.
|
||||
*/
|
||||
height(num, skipListeners) {
|
||||
return this.dimension('height', num, skipListeners);
|
||||
@@ -1056,13 +826,11 @@ class Component {
|
||||
*
|
||||
* @param {number|string} height
|
||||
* Height to set the `Component`s element to.
|
||||
*
|
||||
* @return {Component}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
dimensions(width, height) {
|
||||
// Skip resize listeners on width for optimization
|
||||
return this.width(width, true).height(height);
|
||||
this.width(width, true);
|
||||
this.height(height);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1090,9 +858,8 @@ class Component {
|
||||
* @param {boolean} [skipListeners]
|
||||
* Skip resize event trigger
|
||||
*
|
||||
* @return {Component}
|
||||
* - the dimension when getting or 0 if unset
|
||||
* - Returns itself when setting; method can be chained.
|
||||
* @return {number}
|
||||
* The dimension when getting or 0 if unset
|
||||
*/
|
||||
dimension(widthOrHeight, num, skipListeners) {
|
||||
if (num !== undefined) {
|
||||
@@ -1121,8 +888,7 @@ class Component {
|
||||
this.trigger('resize');
|
||||
}
|
||||
|
||||
// Return component
|
||||
return this;
|
||||
return;
|
||||
}
|
||||
|
||||
// Not setting a value, so getting it
|
||||
@@ -1522,6 +1288,81 @@ class Component {
|
||||
return intervalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues up a callback to be passed to requestAnimationFrame (rAF), but
|
||||
* with a few extra bonuses:
|
||||
*
|
||||
* - Supports browsers that do not support rAF by falling back to
|
||||
* {@link Component#setTimeout}.
|
||||
*
|
||||
* - The callback is turned into a {@link Component~GenericCallback} (i.e.
|
||||
* bound to the component).
|
||||
*
|
||||
* - Automatic cancellation of the rAF callback is handled if the component
|
||||
* is disposed before it is called.
|
||||
*
|
||||
* @param {Component~GenericCallback} fn
|
||||
* A function that will be bound to this component and executed just
|
||||
* before the browser's next repaint.
|
||||
*
|
||||
* @return {number}
|
||||
* Returns an rAF ID that gets used to identify the timeout. It can
|
||||
* also be used in {@link Component#cancelAnimationFrame} to cancel
|
||||
* the animation frame callback.
|
||||
*
|
||||
* @listens Component#dispose
|
||||
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
|
||||
*/
|
||||
requestAnimationFrame(fn) {
|
||||
if (this.supportsRaf_) {
|
||||
fn = Fn.bind(this, fn);
|
||||
|
||||
const id = window.requestAnimationFrame(fn);
|
||||
const disposeFn = () => this.cancelAnimationFrame(id);
|
||||
|
||||
disposeFn.guid = `vjs-raf-${id}`;
|
||||
this.on('dispose', disposeFn);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
// Fall back to using a timer.
|
||||
return this.setTimeout(fn, 1000 / 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a queued callback passed to {@link Component#requestAnimationFrame}
|
||||
* (rAF).
|
||||
*
|
||||
* If you queue an rAF callback via {@link Component#requestAnimationFrame},
|
||||
* use this function instead of `window.cancelAnimationFrame`. If you don't,
|
||||
* your dispose listener will not get cleaned up until {@link Component#dispose}!
|
||||
*
|
||||
* @param {number} id
|
||||
* The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}.
|
||||
*
|
||||
* @return {number}
|
||||
* Returns the rAF ID that was cleared.
|
||||
*
|
||||
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
|
||||
*/
|
||||
cancelAnimationFrame(id) {
|
||||
if (this.supportsRaf_) {
|
||||
window.cancelAnimationFrame(id);
|
||||
|
||||
const disposeFn = function() {};
|
||||
|
||||
disposeFn.guid = `vjs-raf-${id}`;
|
||||
|
||||
this.off('dispose', disposeFn);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
// Fall back to using a timer.
|
||||
return this.clearTimeout(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a `Component` with `videojs` given the name and the component.
|
||||
*
|
||||
@@ -1535,15 +1376,34 @@ class Component {
|
||||
* @param {string} name
|
||||
* The name of the `Component` to register.
|
||||
*
|
||||
* @param {Component} comp
|
||||
* @param {Component} ComponentToRegister
|
||||
* The `Component` class to register.
|
||||
*
|
||||
* @return {Component}
|
||||
* The `Component` that was registered.
|
||||
*/
|
||||
static registerComponent(name, comp) {
|
||||
if (!name) {
|
||||
return;
|
||||
static registerComponent(name, ComponentToRegister) {
|
||||
if (typeof name !== 'string' || !name) {
|
||||
throw new Error(`Illegal component name, "${name}"; must be a non-empty string.`);
|
||||
}
|
||||
|
||||
const Tech = Component.getComponent('Tech');
|
||||
|
||||
// We need to make sure this check is only done if Tech has been registered.
|
||||
const isTech = Tech && Tech.isTech(ComponentToRegister);
|
||||
const isComp = Component === ComponentToRegister ||
|
||||
Component.prototype.isPrototypeOf(ComponentToRegister.prototype);
|
||||
|
||||
if (isTech || !isComp) {
|
||||
let reason;
|
||||
|
||||
if (isTech) {
|
||||
reason = 'techs must be registered using Tech.registerTech()';
|
||||
} else {
|
||||
reason = 'must be a Component subclass';
|
||||
}
|
||||
|
||||
throw new Error(`Illegal component, "${name}"; ${reason}.`);
|
||||
}
|
||||
|
||||
name = toTitleCase(name);
|
||||
@@ -1552,23 +1412,26 @@ class Component {
|
||||
Component.components_ = {};
|
||||
}
|
||||
|
||||
if (name === 'Player' && Component.components_[name]) {
|
||||
const Player = Component.components_[name];
|
||||
const Player = Component.getComponent('Player');
|
||||
|
||||
if (name === 'Player' && Player && Player.players) {
|
||||
const players = Player.players;
|
||||
const playerNames = Object.keys(players);
|
||||
|
||||
// If we have players that were disposed, then their name will still be
|
||||
// in Players.players. So, we must loop through and verify that the value
|
||||
// for each item is not null. This allows registration of the Player component
|
||||
// after all players have been disposed or before any were created.
|
||||
if (Player.players &&
|
||||
Object.keys(Player.players).length > 0 &&
|
||||
Object.keys(Player.players).map((playerName) => Player.players[playerName]).every(Boolean)) {
|
||||
throw new Error('Can not register Player component after player has been created');
|
||||
if (players &&
|
||||
playerNames.length > 0 &&
|
||||
playerNames.map((pname) => players[pname]).every(Boolean)) {
|
||||
throw new Error('Can not register Player component after player has been created.');
|
||||
}
|
||||
}
|
||||
|
||||
Component.components_[name] = comp;
|
||||
Component.components_[name] = ComponentToRegister;
|
||||
|
||||
return comp;
|
||||
return ComponentToRegister;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1595,70 +1458,20 @@ class Component {
|
||||
if (Component.components_ && Component.components_[name]) {
|
||||
return Component.components_[name];
|
||||
}
|
||||
|
||||
if (window && window.videojs && window.videojs[name]) {
|
||||
log.warn(`The ${name} component was added to the videojs object when it should be registered using videojs.registerComponent(name, component)`);
|
||||
|
||||
return window.videojs[name];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the constructor using the supplied init method or uses the init of the
|
||||
* parent object.
|
||||
*
|
||||
* @param {Object} [props={}]
|
||||
* An object of properties.
|
||||
*
|
||||
* @return {Object}
|
||||
* the extended object.
|
||||
*
|
||||
* @deprecated since version 5
|
||||
*/
|
||||
static extend(props) {
|
||||
props = props || {};
|
||||
|
||||
log.warn('Component.extend({}) has been deprecated, ' +
|
||||
' use videojs.extend(Component, {}) instead'
|
||||
);
|
||||
|
||||
// Set up the constructor using the supplied init method
|
||||
// or using the init of the parent object
|
||||
// Make sure to check the unobfuscated version for external libs
|
||||
const init = props.init || props.init || this.prototype.init ||
|
||||
this.prototype.init || function() {};
|
||||
// In Resig's simple class inheritance (previously used) the constructor
|
||||
// is a function that calls `this.init.apply(arguments)`
|
||||
// However that would prevent us from using `ParentObject.call(this);`
|
||||
// in a Child constructor because the `this` in `this.init`
|
||||
// would still refer to the Child and cause an infinite loop.
|
||||
// We would instead have to do
|
||||
// `ParentObject.prototype.init.apply(this, arguments);`
|
||||
// Bleh. We're not creating a _super() function, so it's good to keep
|
||||
// the parent constructor reference simple.
|
||||
const subObj = function() {
|
||||
init.apply(this, arguments);
|
||||
};
|
||||
|
||||
// Inherit from this object's prototype
|
||||
subObj.prototype = Object.create(this.prototype);
|
||||
// Reset the constructor property for subObj otherwise
|
||||
// instances of subObj would have the constructor of the parent Object
|
||||
subObj.prototype.constructor = subObj;
|
||||
|
||||
// Make the class extendable
|
||||
subObj.extend = Component.extend;
|
||||
|
||||
// Extend subObj's prototype with functions and other properties from props
|
||||
for (const name in props) {
|
||||
if (props.hasOwnProperty(name)) {
|
||||
subObj.prototype[name] = props[name];
|
||||
}
|
||||
}
|
||||
|
||||
return subObj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not this component supports `requestAnimationFrame`.
|
||||
*
|
||||
* This is exposed primarily for testing purposes.
|
||||
*
|
||||
* @private
|
||||
* @type {Boolean}
|
||||
*/
|
||||
Component.prototype.supportsRaf_ = typeof window.requestAnimationFrame === 'function' &&
|
||||
typeof window.cancelAnimationFrame === 'function';
|
||||
|
||||
Component.registerComponent('Component', Component);
|
||||
|
||||
export default Component;
|
||||
|
||||
@@ -22,7 +22,7 @@ class AudioTrackButton extends TrackButton {
|
||||
* The key/value store of player options.
|
||||
*/
|
||||
constructor(player, options = {}) {
|
||||
options.tracks = player.audioTracks && player.audioTracks();
|
||||
options.tracks = player.audioTracks();
|
||||
|
||||
super(player, options);
|
||||
|
||||
@@ -49,11 +49,7 @@ class AudioTrackButton extends TrackButton {
|
||||
* An array of menu items
|
||||
*/
|
||||
createItems(items = []) {
|
||||
const tracks = this.player_.audioTracks && this.player_.audioTracks();
|
||||
|
||||
if (!tracks) {
|
||||
return items;
|
||||
}
|
||||
const tracks = this.player_.audioTracks();
|
||||
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
const track = tracks[i];
|
||||
|
||||
@@ -33,14 +33,12 @@ class AudioTrackMenuItem extends MenuItem {
|
||||
|
||||
this.track = track;
|
||||
|
||||
if (tracks) {
|
||||
const changeHandler = Fn.bind(this, this.handleTracksChange);
|
||||
const changeHandler = Fn.bind(this, this.handleTracksChange);
|
||||
|
||||
tracks.addEventListener('change', changeHandler);
|
||||
this.on('dispose', () => {
|
||||
tracks.removeEventListener('change', changeHandler);
|
||||
});
|
||||
}
|
||||
tracks.addEventListener('change', changeHandler);
|
||||
this.on('dispose', () => {
|
||||
tracks.removeEventListener('change', changeHandler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,10 +57,6 @@ class AudioTrackMenuItem extends MenuItem {
|
||||
|
||||
super.handleClick(event);
|
||||
|
||||
if (!tracks) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
const track = tracks[i];
|
||||
|
||||
|
||||
@@ -12,9 +12,7 @@ import './time-controls/remaining-time-display.js';
|
||||
import './live-display.js';
|
||||
import './progress-control/progress-control.js';
|
||||
import './fullscreen-toggle.js';
|
||||
import './volume-control/volume-control.js';
|
||||
import './volume-menu-button.js';
|
||||
import './mute-toggle.js';
|
||||
import './volume-panel.js';
|
||||
import './text-track-controls/chapters-button.js';
|
||||
import './text-track-controls/descriptions-button.js';
|
||||
import './text-track-controls/subtitles-button.js';
|
||||
@@ -56,7 +54,7 @@ class ControlBar extends Component {
|
||||
ControlBar.prototype.options_ = {
|
||||
children: [
|
||||
'playToggle',
|
||||
'volumeMenuButton',
|
||||
'volumePanel',
|
||||
'currentTimeDisplay',
|
||||
'timeDivider',
|
||||
'durationDisplay',
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Button from '../button';
|
||||
import Component from '../component';
|
||||
import * as Dom from '../utils/dom.js';
|
||||
import checkVolumeSupport from './volume-control/check-volume-support';
|
||||
|
||||
/**
|
||||
* A button component for muting the audio.
|
||||
@@ -24,23 +25,10 @@ class MuteToggle extends Button {
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
|
||||
this.on(player, 'volumechange', this.update);
|
||||
// hide this control if volume support is missing
|
||||
checkVolumeSupport(this, player);
|
||||
|
||||
// hide mute toggle if the current tech doesn't support volume control
|
||||
if (player.tech_ && player.tech_.featuresVolumeControl === false) {
|
||||
this.addClass('vjs-hidden');
|
||||
}
|
||||
|
||||
this.on(player, 'loadstart', function() {
|
||||
// We need to update the button to account for a default muted state.
|
||||
this.update();
|
||||
|
||||
if (player.tech_.featuresVolumeControl === false) {
|
||||
this.addClass('vjs-hidden');
|
||||
} else {
|
||||
this.removeClass('vjs-hidden');
|
||||
}
|
||||
});
|
||||
this.on(player, ['loadstart', 'volumechange'], this.update);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,7 +53,15 @@ class MuteToggle extends Button {
|
||||
* @listens click
|
||||
*/
|
||||
handleClick(event) {
|
||||
this.player_.muted(this.player_.muted() ? false : true);
|
||||
const vol = this.player_.volume();
|
||||
const lastVolume = this.player_.lastVolume_();
|
||||
|
||||
if (vol === 0) {
|
||||
this.player_.volume(lastVolume);
|
||||
this.player_.muted(false);
|
||||
} else {
|
||||
this.player_.muted(this.player_.muted() ? false : true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,9 +96,9 @@ class MuteToggle extends Button {
|
||||
|
||||
// TODO improve muted icon classes
|
||||
for (let i = 0; i < 4; i++) {
|
||||
Dom.removeElClass(this.el_, `vjs-vol-${i}`);
|
||||
Dom.removeClass(this.el_, `vjs-vol-${i}`);
|
||||
}
|
||||
Dom.addElClass(this.el_, `vjs-vol-${level}`);
|
||||
Dom.addClass(this.el_, `vjs-vol-${level}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class PlayToggle extends Button {
|
||||
|
||||
this.on(player, 'play', this.handlePlay);
|
||||
this.on(player, 'pause', this.handlePause);
|
||||
this.on(player, 'ended', this.handleEnded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,6 +66,7 @@ class PlayToggle extends Button {
|
||||
* @listens Player#play
|
||||
*/
|
||||
handlePlay(event) {
|
||||
this.removeClass('vjs-ended');
|
||||
this.removeClass('vjs-paused');
|
||||
this.addClass('vjs-playing');
|
||||
// change the button text to "Pause"
|
||||
@@ -86,6 +88,17 @@ class PlayToggle extends Button {
|
||||
this.controlText('Play');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the vjs-ended class to the element so it can change appearance
|
||||
*
|
||||
* @method handleEnded
|
||||
*/
|
||||
handleEnded(event) {
|
||||
this.removeClass('vjs-playing');
|
||||
this.addClass('vjs-ended');
|
||||
// change the button text to "Replay"
|
||||
this.controlText('Replay');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
* @file mouse-time-display.js
|
||||
*/
|
||||
import Component from '../../component.js';
|
||||
import * as Dom from '../../utils/dom.js';
|
||||
// import * as Dom from '../../utils/dom.js';
|
||||
import * as Fn from '../../utils/fn.js';
|
||||
import formatTime from '../../utils/format-time.js';
|
||||
import computedStyle from '../../utils/computed-style.js';
|
||||
// import computedStyle from '../../utils/computed-style.js';
|
||||
|
||||
import './time-tooltip';
|
||||
|
||||
/**
|
||||
* The Mouse Time Display component shows the time you will seek to
|
||||
* when hovering over the progress bar
|
||||
* The {@link MouseTimeDisplay} component tracks mouse movement over the
|
||||
* {@link ProgressControl}. It displays an indicator and a {@link TimeTooltip}
|
||||
* indicating the time which is represented by a given point in the
|
||||
* {@link ProgressControl}.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
@@ -19,36 +23,18 @@ class MouseTimeDisplay extends Component {
|
||||
* Creates an instance of this class.
|
||||
*
|
||||
* @param {Player} player
|
||||
* The `Player` that this class should be attached to.
|
||||
* The {@link Player} that this class should be attached to.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* The key/value store of player options.
|
||||
*/
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
|
||||
if (options.playerOptions &&
|
||||
options.playerOptions.controlBar &&
|
||||
options.playerOptions.controlBar.progressControl &&
|
||||
options.playerOptions.controlBar.progressControl.keepTooltipsInside) {
|
||||
this.keepTooltipsInside = options.playerOptions.controlBar.progressControl.keepTooltipsInside;
|
||||
}
|
||||
|
||||
if (this.keepTooltipsInside) {
|
||||
this.tooltip = Dom.createEl('div', {className: 'vjs-time-tooltip'});
|
||||
this.el().appendChild(this.tooltip);
|
||||
this.addClass('vjs-keep-tooltips-inside');
|
||||
}
|
||||
|
||||
this.update(0, 0);
|
||||
|
||||
player.on('ready', () => {
|
||||
this.on(player.controlBar.progressControl.el(), 'mousemove', Fn.throttle(Fn.bind(this, this.handleMouseMove), 25));
|
||||
});
|
||||
this.update = Fn.throttle(Fn.bind(this, this.update), 25);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the `Component`'s DOM element
|
||||
* Create the the DOM element for this class.
|
||||
*
|
||||
* @return {Element}
|
||||
* The element that was created.
|
||||
@@ -60,94 +46,44 @@ class MouseTimeDisplay extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the mouse move event on the `MouseTimeDisplay`.
|
||||
* Enqueues updates to its own DOM as well as the DOM of its
|
||||
* {@link TimeTooltip} child.
|
||||
*
|
||||
* @param {EventTarget~Event} event
|
||||
* The `mousemove` event that caused this to event to run.
|
||||
* @param {Object} seekBarRect
|
||||
* The `ClientRect` for the {@link SeekBar} element.
|
||||
*
|
||||
* @listen mousemove
|
||||
* @param {number} seekBarPoint
|
||||
* A number from 0 to 1, representing a horizontal reference point
|
||||
* from the left edge of the {@link SeekBar}
|
||||
*/
|
||||
handleMouseMove(event) {
|
||||
const duration = this.player_.duration();
|
||||
const newTime = this.calculateDistance(event) * duration;
|
||||
const position = event.pageX - Dom.findElPosition(this.el().parentNode).left;
|
||||
update(seekBarRect, seekBarPoint) {
|
||||
|
||||
this.update(newTime, position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the time and posistion of the `MouseTimeDisplay`.
|
||||
*
|
||||
* @param {number} newTime
|
||||
* Time to change the `MouseTimeDisplay` to.
|
||||
*
|
||||
* @param {nubmer} position
|
||||
* Postion from the left of the in pixels.
|
||||
*/
|
||||
update(newTime, position) {
|
||||
const time = formatTime(newTime, this.player_.duration());
|
||||
|
||||
this.el().style.left = position + 'px';
|
||||
this.el().setAttribute('data-current-time', time);
|
||||
|
||||
if (this.keepTooltipsInside) {
|
||||
const clampedPosition = this.clampPosition_(position);
|
||||
const difference = position - clampedPosition + 1;
|
||||
const tooltipWidth = parseFloat(computedStyle(this.tooltip, 'width'));
|
||||
const tooltipWidthHalf = tooltipWidth / 2;
|
||||
|
||||
this.tooltip.innerHTML = time;
|
||||
this.tooltip.style.right = `-${tooltipWidthHalf - difference}px`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mouse pointers x coordinate in pixels.
|
||||
*
|
||||
* @param {EventTarget~Event} [event]
|
||||
* The `mousemove` event that was passed to this function by
|
||||
* {@link MouseTimeDisplay#handleMouseMove}
|
||||
*
|
||||
* @return {number}
|
||||
* THe x position in pixels of the mouse pointer.
|
||||
*/
|
||||
calculateDistance(event) {
|
||||
return Dom.getPointerPosition(this.el().parentNode, event).x;
|
||||
}
|
||||
|
||||
/**
|
||||
* This takes in a horizontal position for the bar and returns a clamped position.
|
||||
* Clamped position means that it will keep the position greater than half the width
|
||||
* of the tooltip and smaller than the player width minus half the width o the tooltip.
|
||||
* It will only clamp the position if `keepTooltipsInside` option is set.
|
||||
*
|
||||
* @param {number} position
|
||||
* The position the bar wants to be
|
||||
*
|
||||
* @return {number}
|
||||
* The (potentially) new clamped position.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
clampPosition_(position) {
|
||||
if (!this.keepTooltipsInside) {
|
||||
return position;
|
||||
// If there is an existing rAF ID, cancel it so we don't over-queue.
|
||||
if (this.rafId_) {
|
||||
this.cancelAnimationFrame(this.rafId_);
|
||||
}
|
||||
|
||||
const playerWidth = parseFloat(computedStyle(this.player().el(), 'width'));
|
||||
const tooltipWidth = parseFloat(computedStyle(this.tooltip, 'width'));
|
||||
const tooltipWidthHalf = tooltipWidth / 2;
|
||||
let actualPosition = position;
|
||||
this.rafId_ = this.requestAnimationFrame(() => {
|
||||
const duration = this.player_.duration();
|
||||
const content = formatTime(seekBarPoint * duration, duration);
|
||||
|
||||
if (position < tooltipWidthHalf) {
|
||||
actualPosition = Math.ceil(tooltipWidthHalf);
|
||||
} else if (position > (playerWidth - tooltipWidthHalf)) {
|
||||
actualPosition = Math.floor(playerWidth - tooltipWidthHalf);
|
||||
}
|
||||
|
||||
return actualPosition;
|
||||
this.el_.style.left = `${seekBarRect.width * seekBarPoint}px`;
|
||||
this.getChild('timeTooltip').update(seekBarRect, seekBarPoint, content);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default options for `MouseTimeDisplay`
|
||||
*
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
MouseTimeDisplay.prototype.options_ = {
|
||||
children: [
|
||||
'timeTooltip'
|
||||
]
|
||||
};
|
||||
|
||||
Component.registerComponent('MouseTimeDisplay', MouseTimeDisplay);
|
||||
export default MouseTimeDisplay;
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
* @file play-progress-bar.js
|
||||
*/
|
||||
import Component from '../../component.js';
|
||||
import * as Fn from '../../utils/fn.js';
|
||||
import formatTime from '../../utils/format-time.js';
|
||||
|
||||
import './time-tooltip';
|
||||
|
||||
/**
|
||||
* Shows play progress
|
||||
* Used by {@link SeekBar} to display media playback progress as part of the
|
||||
* {@link ProgressControl}.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
@@ -16,31 +18,17 @@ class PlayProgressBar extends Component {
|
||||
* Creates an instance of this class.
|
||||
*
|
||||
* @param {Player} player
|
||||
* The `Player` that this class should be attached to.
|
||||
* The {@link Player} that this class should be attached to.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* The key/value store of player options.
|
||||
*/
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
this.updateDataAttr();
|
||||
this.on(player, 'timeupdate', this.updateDataAttr);
|
||||
player.ready(Fn.bind(this, this.updateDataAttr));
|
||||
|
||||
if (options.playerOptions &&
|
||||
options.playerOptions.controlBar &&
|
||||
options.playerOptions.controlBar.progressControl &&
|
||||
options.playerOptions.controlBar.progressControl.keepTooltipsInside) {
|
||||
this.keepTooltipsInside = options.playerOptions.controlBar.progressControl.keepTooltipsInside;
|
||||
}
|
||||
|
||||
if (this.keepTooltipsInside) {
|
||||
this.addClass('vjs-keep-tooltips-inside');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the `Component`'s DOM element
|
||||
* Create the the DOM element for this class.
|
||||
*
|
||||
* @return {Element}
|
||||
* The element that was created.
|
||||
@@ -53,20 +41,46 @@ class PlayProgressBar extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the data-current-time attribute on the `PlayProgressBar`.
|
||||
* Enqueues updates to its own DOM as well as the DOM of its
|
||||
* {@link TimeTooltip} child.
|
||||
*
|
||||
* @param {EventTarget~Event} [event]
|
||||
* The `timeupdate` event that caused this to run.
|
||||
* @param {Object} seekBarRect
|
||||
* The `ClientRect` for the {@link SeekBar} element.
|
||||
*
|
||||
* @listens Player#timeupdate
|
||||
* @param {number} seekBarPoint
|
||||
* A number from 0 to 1, representing a horizontal reference point
|
||||
* from the left edge of the {@link SeekBar}
|
||||
*/
|
||||
updateDataAttr(event) {
|
||||
const time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime();
|
||||
update(seekBarRect, seekBarPoint) {
|
||||
|
||||
this.el_.setAttribute('data-current-time', formatTime(time, this.player_.duration()));
|
||||
// If there is an existing rAF ID, cancel it so we don't over-queue.
|
||||
if (this.rafId_) {
|
||||
this.cancelAnimationFrame(this.rafId_);
|
||||
}
|
||||
|
||||
this.rafId_ = this.requestAnimationFrame(() => {
|
||||
const time = (this.player_.scrubbing()) ?
|
||||
this.player_.getCache().currentTime :
|
||||
this.player_.currentTime();
|
||||
|
||||
const content = formatTime(time, this.player_.duration());
|
||||
|
||||
this.getChild('timeTooltip').update(seekBarRect, seekBarPoint, content);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Default options for {@link PlayProgressBar}.
|
||||
*
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
PlayProgressBar.prototype.options_ = {
|
||||
children: [
|
||||
'timeTooltip'
|
||||
]
|
||||
};
|
||||
|
||||
Component.registerComponent('PlayProgressBar', PlayProgressBar);
|
||||
export default PlayProgressBar;
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
* @file progress-control.js
|
||||
*/
|
||||
import Component from '../../component.js';
|
||||
import * as Fn from '../../utils/fn.js';
|
||||
import * as Dom from '../../utils/dom.js';
|
||||
|
||||
import './seek-bar.js';
|
||||
import './mouse-time-display.js';
|
||||
|
||||
/**
|
||||
* The Progress Control component contains the seek bar, load progress,
|
||||
@@ -14,6 +15,21 @@ import './mouse-time-display.js';
|
||||
*/
|
||||
class ProgressControl extends Component {
|
||||
|
||||
/**
|
||||
* Creates an instance of this class.
|
||||
*
|
||||
* @param {Player} player
|
||||
* The `Player` that this class should be attached to.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* The key/value store of player options.
|
||||
*/
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
this.handleMouseMove = Fn.throttle(Fn.bind(this, this.handleMouseMove), 25);
|
||||
this.on(this.el_, 'mousemove', this.handleMouseMove);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the `Component`'s DOM element
|
||||
*
|
||||
@@ -25,6 +41,33 @@ class ProgressControl extends Component {
|
||||
className: 'vjs-progress-control vjs-control'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When the mouse moves over the `ProgressControl`, the pointer position
|
||||
* gets passed down to the `MouseTimeDisplay` component.
|
||||
*
|
||||
* @param {EventTarget~Event} event
|
||||
* The `mousemove` event that caused this function to run.
|
||||
*
|
||||
* @listen mousemove
|
||||
*/
|
||||
handleMouseMove(event) {
|
||||
const seekBar = this.getChild('seekBar');
|
||||
const seekBarEl = seekBar.el();
|
||||
const seekBarRect = Dom.getBoundingClientRect(seekBarEl);
|
||||
let seekBarPoint = Dom.getPointerPosition(seekBarEl, event).x;
|
||||
|
||||
// The default skin has a gap on either side of the `SeekBar`. This means
|
||||
// that it's possible to trigger this behavior outside the boundaries of
|
||||
// the `SeekBar`. This ensures we stay within it at all times.
|
||||
if (seekBarPoint > 1) {
|
||||
seekBarPoint = 1;
|
||||
} else if (seekBarPoint < 0) {
|
||||
seekBarPoint = 0;
|
||||
}
|
||||
|
||||
seekBar.getChild('mouseTimeDisplay').update(seekBarRect, seekBarPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,16 +3,20 @@
|
||||
*/
|
||||
import Slider from '../../slider/slider.js';
|
||||
import Component from '../../component.js';
|
||||
import * as Dom from '../../utils/dom.js';
|
||||
import * as Fn from '../../utils/fn.js';
|
||||
import formatTime from '../../utils/format-time.js';
|
||||
import computedStyle from '../../utils/computed-style.js';
|
||||
|
||||
import './load-progress-bar.js';
|
||||
import './play-progress-bar.js';
|
||||
import './tooltip-progress-bar.js';
|
||||
import './mouse-time-display.js';
|
||||
|
||||
// The number of seconds the `step*` functions move the timeline.
|
||||
const STEP_SECONDS = 5;
|
||||
|
||||
/**
|
||||
* Seek Bar and holder for the progress bars
|
||||
* Seek bar and container for the progress bars. Uses {@link PlayProgressBar}
|
||||
* as its `bar`.
|
||||
*
|
||||
* @extends Slider
|
||||
*/
|
||||
@@ -29,20 +33,8 @@ class SeekBar extends Slider {
|
||||
*/
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
this.on(player, 'timeupdate', this.updateProgress);
|
||||
this.on(player, 'ended', this.updateProgress);
|
||||
player.ready(Fn.bind(this, this.updateProgress));
|
||||
|
||||
if (options.playerOptions &&
|
||||
options.playerOptions.controlBar &&
|
||||
options.playerOptions.controlBar.progressControl &&
|
||||
options.playerOptions.controlBar.progressControl.keepTooltipsInside) {
|
||||
this.keepTooltipsInside = options.playerOptions.controlBar.progressControl.keepTooltipsInside;
|
||||
}
|
||||
|
||||
if (this.keepTooltipsInside) {
|
||||
this.tooltipProgressBar = this.addChild('TooltipProgressBar');
|
||||
}
|
||||
this.update = Fn.throttle(Fn.bind(this, this.update), 50);
|
||||
this.on(player, ['timeupdate', 'ended'], this.update);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +52,7 @@ class SeekBar extends Slider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the seek bars tooltip and width.
|
||||
* Update the seek bar's UI.
|
||||
*
|
||||
* @param {EventTarget~Event} [event]
|
||||
* The `timeupdate` or `ended` event that caused this to run.
|
||||
@@ -68,47 +60,41 @@ class SeekBar extends Slider {
|
||||
* @listens Player#timeupdate
|
||||
* @listens Player#ended
|
||||
*/
|
||||
updateProgress(event) {
|
||||
this.updateAriaAttributes(this.el_);
|
||||
update() {
|
||||
const percent = super.update();
|
||||
const duration = this.player_.duration();
|
||||
|
||||
if (this.keepTooltipsInside) {
|
||||
this.updateAriaAttributes(this.tooltipProgressBar.el_);
|
||||
this.tooltipProgressBar.el_.style.width = this.bar.el_.style.width;
|
||||
|
||||
const playerWidth = parseFloat(computedStyle(this.player().el(), 'width'));
|
||||
const tooltipWidth = parseFloat(computedStyle(this.tooltipProgressBar.tooltip, 'width'));
|
||||
const tooltipStyle = this.tooltipProgressBar.el().style;
|
||||
|
||||
tooltipStyle.maxWidth = Math.floor(playerWidth - (tooltipWidth / 2)) + 'px';
|
||||
tooltipStyle.minWidth = Math.ceil(tooltipWidth / 2) + 'px';
|
||||
tooltipStyle.right = `-${tooltipWidth / 2}px`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ARIA accessibility attributes
|
||||
*
|
||||
* @param {Element} el
|
||||
* The element to update with aria accessibility attributes.
|
||||
*/
|
||||
updateAriaAttributes(el) {
|
||||
// Allows for smooth scrubbing, when player can't keep up.
|
||||
const time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime();
|
||||
const time = (this.player_.scrubbing()) ?
|
||||
this.player_.getCache().currentTime :
|
||||
this.player_.currentTime();
|
||||
|
||||
// machine readable value of progress bar (percentage complete)
|
||||
el.setAttribute('aria-valuenow', (this.getPercent() * 100).toFixed(2));
|
||||
this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2));
|
||||
|
||||
// human readable value of progress bar (time complete)
|
||||
el.setAttribute('aria-valuetext', formatTime(time, this.player_.duration()));
|
||||
this.el_.setAttribute('aria-valuetext', formatTime(time, duration));
|
||||
|
||||
// Update the `PlayProgressBar`.
|
||||
this.bar.update(Dom.getBoundingClientRect(this.el_), percent);
|
||||
|
||||
return percent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get percentage of video played
|
||||
* Get the percentage of media played so far.
|
||||
*
|
||||
* @return {number}
|
||||
* The percentage played
|
||||
* The percentage of media played so far (0 to 1).
|
||||
*/
|
||||
getPercent() {
|
||||
const percent = this.player_.currentTime() / this.player_.duration();
|
||||
|
||||
// Allows for smooth scrubbing, when player can't keep up.
|
||||
const time = (this.player_.scrubbing()) ?
|
||||
this.player_.getCache().currentTime :
|
||||
this.player_.currentTime();
|
||||
|
||||
const percent = time / this.player_.duration();
|
||||
|
||||
return percent >= 1 ? 1 : percent;
|
||||
}
|
||||
@@ -171,18 +157,15 @@ class SeekBar extends Slider {
|
||||
* Move more quickly fast forward for keyboard-only users
|
||||
*/
|
||||
stepForward() {
|
||||
// more quickly fast forward for keyboard-only users
|
||||
this.player_.currentTime(this.player_.currentTime() + 5);
|
||||
this.player_.currentTime(this.player_.currentTime() + STEP_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move more quickly rewind for keyboard-only users
|
||||
*/
|
||||
stepBack() {
|
||||
// more quickly rewind for keyboard-only users
|
||||
this.player_.currentTime(this.player_.currentTime() - 5);
|
||||
this.player_.currentTime(this.player_.currentTime() - STEP_SECONDS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @file time-tooltip.js
|
||||
*/
|
||||
import Component from '../../component';
|
||||
import * as Dom from '../../utils/dom.js';
|
||||
|
||||
/**
|
||||
* Time tooltips display a time above the progress bar.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class TimeTooltip extends Component {
|
||||
|
||||
/**
|
||||
* Create the time tooltip DOM element
|
||||
*
|
||||
* @return {Element}
|
||||
* The element that was created.
|
||||
*/
|
||||
createEl() {
|
||||
return super.createEl('div', {
|
||||
className: 'vjs-time-tooltip'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the position of the time tooltip relative to the `SeekBar`.
|
||||
*
|
||||
* @param {Object} seekBarRect
|
||||
* The `ClientRect` for the {@link SeekBar} element.
|
||||
*
|
||||
* @param {number} seekBarPoint
|
||||
* A number from 0 to 1, representing a horizontal reference point
|
||||
* from the left edge of the {@link SeekBar}
|
||||
*/
|
||||
update(seekBarRect, seekBarPoint, content) {
|
||||
const tooltipRect = Dom.getBoundingClientRect(this.el_);
|
||||
const playerRect = Dom.getBoundingClientRect(this.player_.el());
|
||||
const seekBarPointPx = seekBarRect.width * seekBarPoint;
|
||||
|
||||
// do nothing if either rect isn't available
|
||||
// for example, if the player isn't in the DOM for testing
|
||||
if (!playerRect || !tooltipRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This is the space left of the `seekBarPoint` available within the bounds
|
||||
// of the player. We calculate any gap between the left edge of the player
|
||||
// and the left edge of the `SeekBar` and add the number of pixels in the
|
||||
// `SeekBar` before hitting the `seekBarPoint`
|
||||
const spaceLeftOfPoint = (seekBarRect.left - playerRect.left) + seekBarPointPx;
|
||||
|
||||
// This is the space right of the `seekBarPoint` available within the bounds
|
||||
// of the player. We calculate the number of pixels from the `seekBarPoint`
|
||||
// to the right edge of the `SeekBar` and add to that any gap between the
|
||||
// right edge of the `SeekBar` and the player.
|
||||
const spaceRightOfPoint = (seekBarRect.width - seekBarPointPx) +
|
||||
(playerRect.right - seekBarRect.right);
|
||||
|
||||
// This is the number of pixels by which the tooltip will need to be pulled
|
||||
// further to the right to center it over the `seekBarPoint`.
|
||||
let pullTooltipBy = tooltipRect.width / 2;
|
||||
|
||||
// Adjust the `pullTooltipBy` distance to the left or right depending on
|
||||
// the results of the space calculations above.
|
||||
if (spaceLeftOfPoint < pullTooltipBy) {
|
||||
pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
|
||||
} else if (spaceRightOfPoint < pullTooltipBy) {
|
||||
pullTooltipBy = spaceRightOfPoint;
|
||||
}
|
||||
|
||||
// Due to the imprecision of decimal/ratio based calculations and varying
|
||||
// rounding behaviors, there are cases where the spacing adjustment is off
|
||||
// by a pixel or two. This adds insurance to these calculations.
|
||||
if (pullTooltipBy < 0) {
|
||||
pullTooltipBy = 0;
|
||||
} else if (pullTooltipBy > tooltipRect.width) {
|
||||
pullTooltipBy = tooltipRect.width;
|
||||
}
|
||||
|
||||
this.el_.style.right = `-${pullTooltipBy}px`;
|
||||
Dom.textContent(this.el_, content);
|
||||
}
|
||||
}
|
||||
|
||||
Component.registerComponent('TimeTooltip', TimeTooltip);
|
||||
export default TimeTooltip;
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* @file play-progress-bar.js
|
||||
*/
|
||||
import Component from '../../component.js';
|
||||
import * as Fn from '../../utils/fn.js';
|
||||
import formatTime from '../../utils/format-time.js';
|
||||
|
||||
/**
|
||||
* Shows play progress
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class TooltipProgressBar extends Component {
|
||||
|
||||
/**
|
||||
* Creates an instance of this class.
|
||||
*
|
||||
* @param {Player} player
|
||||
* The `Player` that this class should be attached to.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* The key/value store of player options.
|
||||
*/
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
this.updateDataAttr();
|
||||
this.on(player, 'timeupdate', this.updateDataAttr);
|
||||
player.ready(Fn.bind(this, this.updateDataAttr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the `Component`'s DOM element
|
||||
*
|
||||
* @return {Element}
|
||||
* The element that was created.
|
||||
*/
|
||||
createEl() {
|
||||
const el = super.createEl('div', {
|
||||
className: 'vjs-tooltip-progress-bar vjs-slider-bar',
|
||||
innerHTML: `<div class="vjs-time-tooltip"></div>
|
||||
<span class="vjs-control-text"><span>${this.localize('Progress')}</span>: 0%</span>`
|
||||
});
|
||||
|
||||
this.tooltip = el.querySelector('.vjs-time-tooltip');
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updatet the data-current-time attribute for TooltipProgressBar
|
||||
*
|
||||
* @param {EventTarget~Event} [event]
|
||||
* The `timeupdate` event that caused this function to run.
|
||||
*
|
||||
* @listens Player#timeupdate
|
||||
*/
|
||||
updateDataAttr(event) {
|
||||
const time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime();
|
||||
const formattedTime = formatTime(time, this.player_.duration());
|
||||
|
||||
this.el_.setAttribute('data-current-time', formattedTime);
|
||||
this.tooltip.innerHTML = formattedTime;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component.registerComponent('TooltipProgressBar', TooltipProgressBar);
|
||||
export default TooltipProgressBar;
|
||||
@@ -29,15 +29,12 @@ class DescriptionsButton extends TextTrackButton {
|
||||
this.el_.setAttribute('aria-label', 'Descriptions Menu');
|
||||
|
||||
const tracks = player.textTracks();
|
||||
const changeHandler = Fn.bind(this, this.handleTracksChange);
|
||||
|
||||
if (tracks) {
|
||||
const changeHandler = Fn.bind(this, this.handleTracksChange);
|
||||
|
||||
tracks.addEventListener('change', changeHandler);
|
||||
this.on('dispose', function() {
|
||||
tracks.removeEventListener('change', changeHandler);
|
||||
});
|
||||
}
|
||||
tracks.addEventListener('change', changeHandler);
|
||||
this.on('dispose', function() {
|
||||
tracks.removeEventListener('change', changeHandler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,10 +43,6 @@ class TextTrackButton extends TrackButton {
|
||||
|
||||
const tracks = this.player_.textTracks();
|
||||
|
||||
if (!tracks) {
|
||||
return items;
|
||||
}
|
||||
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
const track = tracks[i];
|
||||
|
||||
|
||||
@@ -34,15 +34,12 @@ class TextTrackMenuItem extends MenuItem {
|
||||
super(player, options);
|
||||
|
||||
this.track = track;
|
||||
const changeHandler = Fn.bind(this, this.handleTracksChange);
|
||||
|
||||
if (tracks) {
|
||||
const changeHandler = Fn.bind(this, this.handleTracksChange);
|
||||
|
||||
tracks.addEventListener('change', changeHandler);
|
||||
this.on('dispose', function() {
|
||||
tracks.removeEventListener('change', changeHandler);
|
||||
});
|
||||
}
|
||||
tracks.addEventListener('change', changeHandler);
|
||||
this.on('dispose', function() {
|
||||
tracks.removeEventListener('change', changeHandler);
|
||||
});
|
||||
|
||||
// iOS7 doesn't dispatch change events to TextTrackLists when an
|
||||
// associated track's mode changes. Without something like
|
||||
@@ -50,7 +47,7 @@ class TextTrackMenuItem extends MenuItem {
|
||||
// possible to detect changes to the mode attribute and polyfill
|
||||
// the change event. As a poor substitute, we manually dispatch
|
||||
// change events whenever the controls modify the mode.
|
||||
if (tracks && tracks.onchange === undefined) {
|
||||
if (tracks.onchange === undefined) {
|
||||
let event;
|
||||
|
||||
this.on(['tap', 'click'], function() {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Check if volume control is supported and if it isn't hide the
|
||||
* `Component` that was passed using the `vjs-hidden` class.
|
||||
*
|
||||
* @param {Component} self
|
||||
* The component that should be hidden if volume is unsupported
|
||||
*
|
||||
* @param {Player} player
|
||||
* A reference to the player
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
const checkVolumeSupport = function(self, player) {
|
||||
// hide volume controls when they're not supported by the current tech
|
||||
if (player.tech_ && !player.tech_.featuresVolumeControl) {
|
||||
self.addClass('vjs-hidden');
|
||||
}
|
||||
|
||||
self.on(player, 'loadstart', function() {
|
||||
if (!player.tech_.featuresVolumeControl) {
|
||||
self.addClass('vjs-hidden');
|
||||
} else {
|
||||
self.removeClass('vjs-hidden');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default checkVolumeSupport;
|
||||
@@ -3,7 +3,6 @@
|
||||
*/
|
||||
import Slider from '../../slider/slider.js';
|
||||
import Component from '../../component.js';
|
||||
import * as Fn from '../../utils/fn.js';
|
||||
|
||||
// Required children
|
||||
import './volume-level.js';
|
||||
@@ -26,8 +25,9 @@ class VolumeBar extends Slider {
|
||||
*/
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
this.on('slideractive', this.updateLastVolume_);
|
||||
this.on(player, 'volumechange', this.updateARIAAttributes);
|
||||
player.ready(Fn.bind(this, this.updateARIAAttributes));
|
||||
player.ready(() => this.updateARIAAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +40,8 @@ class VolumeBar extends Slider {
|
||||
return super.createEl('div', {
|
||||
className: 'vjs-volume-bar vjs-slider-bar'
|
||||
}, {
|
||||
'aria-label': 'volume level'
|
||||
'aria-label': 'volume level',
|
||||
'aria-live': 'polite'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,6 +112,24 @@ class VolumeBar extends Slider {
|
||||
this.el_.setAttribute('aria-valuetext', volume + '%');
|
||||
}
|
||||
|
||||
/**
|
||||
* When user starts dragging the VolumeBar, store the volume and listen for
|
||||
* the end of the drag. When the drag ends, if the volume was set to zero,
|
||||
* set lastVolume to the stored volume.
|
||||
*
|
||||
* @listens slideractive
|
||||
* @private
|
||||
*/
|
||||
updateLastVolume_() {
|
||||
const volumeBeforeDrag = this.player_.volume();
|
||||
|
||||
this.one('sliderinactive', () => {
|
||||
if (this.player_.volume() === 0) {
|
||||
this.player_.lastVolume_(volumeBeforeDrag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
* @file volume-control.js
|
||||
*/
|
||||
import Component from '../../component.js';
|
||||
import checkVolumeSupport from './check-volume-support';
|
||||
import {isPlain} from '../../utils/obj';
|
||||
import { throttle, bind } from '../../utils/fn.js';
|
||||
|
||||
// Required children
|
||||
import './volume-bar.js';
|
||||
@@ -22,19 +25,38 @@ class VolumeControl extends Component {
|
||||
* @param {Object} [options={}]
|
||||
* The key/value store of player options.
|
||||
*/
|
||||
constructor(player, options) {
|
||||
constructor(player, options = {}) {
|
||||
options.vertical = options.vertical || false;
|
||||
|
||||
// Pass the vertical option down to the VolumeBar if
|
||||
// the VolumeBar is turned on.
|
||||
if (typeof options.volumeBar === 'undefined' || isPlain(options.volumeBar)) {
|
||||
options.volumeBar = options.volumeBar || {};
|
||||
options.volumeBar.vertical = options.vertical;
|
||||
}
|
||||
|
||||
super(player, options);
|
||||
|
||||
// hide volume controls when they're not supported by the current tech
|
||||
if (player.tech_ && player.tech_.featuresVolumeControl === false) {
|
||||
this.addClass('vjs-hidden');
|
||||
}
|
||||
this.on(player, 'loadstart', function() {
|
||||
if (player.tech_.featuresVolumeControl === false) {
|
||||
this.addClass('vjs-hidden');
|
||||
} else {
|
||||
this.removeClass('vjs-hidden');
|
||||
}
|
||||
// hide this control if volume support is missing
|
||||
checkVolumeSupport(this, player);
|
||||
|
||||
this.throttledHandleMouseMove = throttle(bind(this, this.handleMouseMove), 25);
|
||||
|
||||
this.on('mousedown', this.handleMouseDown);
|
||||
this.on('touchstart', this.handleMouseDown);
|
||||
|
||||
// while the slider is active (the mouse has been pressed down and
|
||||
// is dragging) or in focus we do not want to hide the VolumeBar
|
||||
this.on(this.volumeBar, ['focus', 'slideractive'], () => {
|
||||
this.volumeBar.addClass('vjs-slider-active');
|
||||
this.addClass('vjs-slider-active');
|
||||
this.trigger('slideractive');
|
||||
});
|
||||
|
||||
this.on(this.volumeBar, ['blur', 'sliderinactive'], () => {
|
||||
this.volumeBar.removeClass('vjs-slider-active');
|
||||
this.removeClass('vjs-slider-active');
|
||||
this.trigger('sliderinactive');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,11 +67,65 @@ class VolumeControl extends Component {
|
||||
* The element that was created.
|
||||
*/
|
||||
createEl() {
|
||||
let orientationClass = 'vjs-volume-horizontal';
|
||||
|
||||
if (this.options_.vertical) {
|
||||
orientationClass = 'vjs-volume-vertical';
|
||||
}
|
||||
|
||||
return super.createEl('div', {
|
||||
className: 'vjs-volume-control vjs-control'
|
||||
className: `vjs-volume-control vjs-control ${orientationClass}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle `mousedown` or `touchstart` events on the `VolumeControl`.
|
||||
*
|
||||
* @param {EventTarget~Event} event
|
||||
* `mousedown` or `touchstart` event that triggered this function
|
||||
*
|
||||
* @listens mousedown
|
||||
* @listens touchstart
|
||||
*/
|
||||
handleMouseDown(event) {
|
||||
const doc = this.el_.ownerDocument;
|
||||
|
||||
this.on(doc, 'mousemove', this.throttledHandleMouseMove);
|
||||
this.on(doc, 'touchmove', this.throttledHandleMouseMove);
|
||||
this.on(doc, 'mouseup', this.handleMouseUp);
|
||||
this.on(doc, 'touchend', this.handleMouseUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle `mouseup` or `touchend` events on the `VolumeControl`.
|
||||
*
|
||||
* @param {EventTarget~Event} event
|
||||
* `mouseup` or `touchend` event that triggered this function.
|
||||
*
|
||||
* @listens touchend
|
||||
* @listens mouseup
|
||||
*/
|
||||
handleMouseUp(event) {
|
||||
const doc = this.el_.ownerDocument;
|
||||
|
||||
this.off(doc, 'mousemove', this.throttledHandleMouseMove);
|
||||
this.off(doc, 'touchmove', this.throttledHandleMouseMove);
|
||||
this.off(doc, 'mouseup', this.handleMouseUp);
|
||||
this.off(doc, 'touchend', this.handleMouseUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle `mousedown` or `touchstart` events on the `VolumeControl`.
|
||||
*
|
||||
* @param {EventTarget~Event} event
|
||||
* `mousedown` or `touchstart` event that triggered this function
|
||||
*
|
||||
* @listens mousedown
|
||||
* @listens touchstart
|
||||
*/
|
||||
handleMouseMove(event) {
|
||||
this.volumeBar.handleMouseMove(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
/**
|
||||
* @file volume-menu-button.js
|
||||
*/
|
||||
import * as Fn from '../utils/fn.js';
|
||||
import Component from '../component.js';
|
||||
import Popup from '../popup/popup.js';
|
||||
import PopupButton from '../popup/popup-button.js';
|
||||
import MuteToggle from './mute-toggle.js';
|
||||
import VolumeBar from './volume-control/volume-bar.js';
|
||||
|
||||
/**
|
||||
* Button for volume popup
|
||||
*
|
||||
* @extends PopupButton
|
||||
*/
|
||||
class VolumeMenuButton extends PopupButton {
|
||||
|
||||
/**
|
||||
* Creates an instance of this class.
|
||||
*
|
||||
* @param {Player} player
|
||||
* The `Player` that this class should be attached to.
|
||||
*
|
||||
* @param {Object} [options={}]
|
||||
* The key/value store of player options.
|
||||
*/
|
||||
constructor(player, options = {}) {
|
||||
// Default to inline
|
||||
if (options.inline === undefined) {
|
||||
options.inline = true;
|
||||
}
|
||||
|
||||
// If the vertical option isn't passed at all, default to true.
|
||||
if (options.vertical === undefined) {
|
||||
// If an inline volumeMenuButton is used, we should default to using
|
||||
// a horizontal slider for obvious reasons.
|
||||
if (options.inline) {
|
||||
options.vertical = false;
|
||||
} else {
|
||||
options.vertical = true;
|
||||
}
|
||||
}
|
||||
|
||||
// The vertical option needs to be set on the volumeBar as well,
|
||||
// since that will need to be passed along to the VolumeBar constructor
|
||||
options.volumeBar = options.volumeBar || {};
|
||||
options.volumeBar.vertical = !!options.vertical;
|
||||
|
||||
super(player, options);
|
||||
|
||||
// Same listeners as MuteToggle
|
||||
this.on(player, 'volumechange', this.volumeUpdate);
|
||||
this.on(player, 'loadstart', this.volumeUpdate);
|
||||
|
||||
// hide mute toggle if the current tech doesn't support volume control
|
||||
function updateVisibility() {
|
||||
if (player.tech_ && player.tech_.featuresVolumeControl === false) {
|
||||
this.addClass('vjs-hidden');
|
||||
} else {
|
||||
this.removeClass('vjs-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
updateVisibility.call(this);
|
||||
this.on(player, 'loadstart', updateVisibility);
|
||||
|
||||
this.on(this.volumeBar, ['slideractive', 'focus'], function() {
|
||||
this.addClass('vjs-slider-active');
|
||||
});
|
||||
|
||||
this.on(this.volumeBar, ['sliderinactive', 'blur'], function() {
|
||||
this.removeClass('vjs-slider-active');
|
||||
});
|
||||
|
||||
this.on(this.volumeBar, ['focus'], function() {
|
||||
this.addClass('vjs-lock-showing');
|
||||
});
|
||||
|
||||
this.on(this.volumeBar, ['blur'], function() {
|
||||
this.removeClass('vjs-lock-showing');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the default DOM `className`.
|
||||
*
|
||||
* @return {string}
|
||||
* The DOM `className` for this object.
|
||||
*/
|
||||
buildCSSClass() {
|
||||
let orientationClass = '';
|
||||
|
||||
if (this.options_.vertical) {
|
||||
orientationClass = 'vjs-volume-menu-button-vertical';
|
||||
} else {
|
||||
orientationClass = 'vjs-volume-menu-button-horizontal';
|
||||
}
|
||||
|
||||
return `vjs-volume-menu-button ${super.buildCSSClass()} ${orientationClass}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the VolumeMenuButton popup
|
||||
*
|
||||
* @return {Popup}
|
||||
* The popup that was created
|
||||
*/
|
||||
createPopup() {
|
||||
const popup = new Popup(this.player_, {
|
||||
contentElType: 'div'
|
||||
});
|
||||
|
||||
const vb = new VolumeBar(this.player_, this.options_.volumeBar);
|
||||
|
||||
popup.addChild(vb);
|
||||
|
||||
this.menuContent = popup;
|
||||
this.volumeBar = vb;
|
||||
|
||||
this.attachVolumeBarEvents();
|
||||
|
||||
return popup;
|
||||
}
|
||||
|
||||
/**
|
||||
* This gets called when an `VolumeMenuButton` is "clicked". See
|
||||
* {@link ClickableComponent} for more detailed information on what a click can be.
|
||||
*
|
||||
* @param {EventTarget~Event} [event]
|
||||
* The `keydown`, `tap`, or `click` event that caused this function to be
|
||||
* called.
|
||||
*
|
||||
* @listens tap
|
||||
* @listens click
|
||||
*/
|
||||
handleClick(event) {
|
||||
MuteToggle.prototype.handleClick.call(this);
|
||||
super.handleClick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add events listeners to the created `VolumeBar`.
|
||||
*/
|
||||
attachVolumeBarEvents() {
|
||||
this.menuContent.on(['mousedown', 'touchdown'], Fn.bind(this, this.handleMouseDown));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the `mousedown` and `touchdown` events on the `VolumeBar`
|
||||
*
|
||||
* @param {EventTarget~Event} [event]
|
||||
* The `mousedown` or `touchdown` event that caused this to run.
|
||||
*
|
||||
* @listens mousedown
|
||||
* @listens touchdown
|
||||
*/
|
||||
handleMouseDown(event) {
|
||||
this.on(['mousemove', 'touchmove'], Fn.bind(this.volumeBar, this.volumeBar.handleMouseMove));
|
||||
this.on(this.el_.ownerDocument, ['mouseup', 'touchend'], this.handleMouseUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the `mouseup` and `touchend` events on the `VolumeBar`
|
||||
*
|
||||
* @param {EventTarget~Event} [event]
|
||||
* The `mouseup` or `touchend` event that caused this to run.
|
||||
*
|
||||
* @listens mouseup
|
||||
* @listens touchend
|
||||
*/
|
||||
handleMouseUp(event) {
|
||||
this.off(['mousemove', 'touchmove'], Fn.bind(this.volumeBar, this.volumeBar.handleMouseMove));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @borrows MuteToggle#update as VolumeMenuButton#volumeUpdate
|
||||
*/
|
||||
VolumeMenuButton.prototype.volumeUpdate = MuteToggle.prototype.update;
|
||||
|
||||
/**
|
||||
* The text that should display over the `VolumeMenuButton`s controls. Added for localization.
|
||||
*
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
VolumeMenuButton.prototype.controlText_ = 'Mute';
|
||||
|
||||
Component.registerComponent('VolumeMenuButton', VolumeMenuButton);
|
||||
export default VolumeMenuButton;
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* @file volume-control.js
|
||||
*/
|
||||
import Component from '../component.js';
|
||||
import checkVolumeSupport from './volume-control/check-volume-support';
|
||||
import {isPlain} from '../utils/obj';
|
||||
|
||||
// Required children
|
||||
import './volume-control/volume-control.js';
|
||||
import './mute-toggle.js';
|
||||
|
||||
/**
|
||||
* A Component to contain the MuteToggle and VolumeControl so that
|
||||
* they can work together.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class VolumePanel extends Component {
|
||||
|
||||
/**
|
||||
* Creates an instance of this class.
|
||||
*
|
||||
* @param {Player} player
|
||||
* The `Player` that this class should be attached to.
|
||||
*
|
||||
* @param {Object} [options={}]
|
||||
* The key/value store of player options.
|
||||
*/
|
||||
constructor(player, options = {}) {
|
||||
if (typeof options.inline !== 'undefined') {
|
||||
options.inline = options.inline;
|
||||
} else {
|
||||
options.inline = true;
|
||||
}
|
||||
|
||||
// pass the inline option down to the VolumeControl as vertical if
|
||||
// the VolumeControl is on.
|
||||
if (typeof options.volumeControl === 'undefined' || isPlain(options.volumeControl)) {
|
||||
options.volumeControl = options.volumeControl || {};
|
||||
options.volumeControl.vertical = !options.inline;
|
||||
}
|
||||
|
||||
super(player, options);
|
||||
|
||||
// hide this control if volume support is missing
|
||||
checkVolumeSupport(this, player);
|
||||
|
||||
// while the slider is active (the mouse has been pressed down and
|
||||
// is dragging) or in focus we do not want to hide the VolumeBar
|
||||
this.on(this.volumeControl, ['slideractive'], this.sliderActive_);
|
||||
this.on(this.muteToggle, 'focus', this.sliderActive_);
|
||||
|
||||
this.on(this.volumeControl, ['sliderinactive'], this.sliderInactive_);
|
||||
this.on(this.muteToggle, 'blur', this.sliderInactive_);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add vjs-slider-active class to the VolumePanel
|
||||
*
|
||||
* @listens VolumeControl#slideractive
|
||||
* @private
|
||||
*/
|
||||
sliderActive_() {
|
||||
this.addClass('vjs-slider-active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes vjs-slider-active class to the VolumePanel
|
||||
*
|
||||
* @listens VolumeControl#sliderinactive
|
||||
* @private
|
||||
*/
|
||||
sliderInactive_() {
|
||||
this.removeClass('vjs-slider-active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the `Component`'s DOM element
|
||||
*
|
||||
* @return {Element}
|
||||
* The element that was created.
|
||||
*/
|
||||
createEl() {
|
||||
let orientationClass = 'vjs-volume-panel-horizontal';
|
||||
|
||||
if (!this.options_.inline) {
|
||||
orientationClass = 'vjs-volume-panel-vertical';
|
||||
}
|
||||
|
||||
return super.createEl('div', {
|
||||
className: `vjs-volume-panel vjs-control ${orientationClass}`
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Default options for the `VolumeControl`
|
||||
*
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
VolumePanel.prototype.options_ = {
|
||||
children: [
|
||||
'muteToggle',
|
||||
'volumeControl'
|
||||
]
|
||||
};
|
||||
|
||||
Component.registerComponent('VolumePanel', VolumePanel);
|
||||
export default VolumePanel;
|
||||
+2
-9
@@ -1,7 +1,4 @@
|
||||
import log from './utils/log';
|
||||
import {isObject} from './utils/obj';
|
||||
|
||||
/**
|
||||
/*
|
||||
* @file extend.js
|
||||
* @module extend
|
||||
*/
|
||||
@@ -59,11 +56,7 @@ const extendFn = function(superClass, subClassMethods = {}) {
|
||||
|
||||
let methods = {};
|
||||
|
||||
if (isObject(subClassMethods)) {
|
||||
if (typeof subClassMethods.init === 'function') {
|
||||
log.warn('Constructor logic via init() is deprecated; please use constructor() instead.');
|
||||
subClassMethods.constructor = subClassMethods.init;
|
||||
}
|
||||
if (typeof subClassMethods === 'object') {
|
||||
if (subClassMethods.constructor !== Object.prototype.constructor) {
|
||||
subClass = subClassMethods.constructor;
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class MenuButton extends ClickableComponent {
|
||||
});
|
||||
|
||||
menu.children_.unshift(title);
|
||||
Dom.insertElFirst(title, menu.contentEl());
|
||||
Dom.prependTo(title, menu.contentEl());
|
||||
}
|
||||
|
||||
this.items = this.createItems();
|
||||
@@ -245,9 +245,6 @@ class MenuButton extends ClickableComponent {
|
||||
|
||||
/**
|
||||
* Disable the `MenuButton`. Don't allow it to be clicked.
|
||||
*
|
||||
* @return {MenuButton}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
disable() {
|
||||
// Unpress, but don't force focus on this button
|
||||
@@ -257,19 +254,15 @@ class MenuButton extends ClickableComponent {
|
||||
|
||||
this.enabled_ = false;
|
||||
|
||||
return super.disable();
|
||||
super.disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the `MenuButton`. Allow it to be clicked.
|
||||
*
|
||||
* @return {MenuButton}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
enable() {
|
||||
this.enabled_ = true;
|
||||
|
||||
return super.enable();
|
||||
super.enable();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* @file mixins/evented.js
|
||||
* @module evented
|
||||
*/
|
||||
import * as Dom from '../utils/dom';
|
||||
import * as Events from '../utils/events';
|
||||
import * as Fn from '../utils/fn';
|
||||
import * as Obj from '../utils/obj';
|
||||
import EventTarget from '../event-target';
|
||||
|
||||
/**
|
||||
* Returns whether or not an object has had the evented mixin applied.
|
||||
*
|
||||
* @param {Object} object
|
||||
* An object to test.
|
||||
*
|
||||
* @return {boolean}
|
||||
* Whether or not the object appears to be evented.
|
||||
*/
|
||||
const isEvented = (object) =>
|
||||
object instanceof EventTarget ||
|
||||
!!object.eventBusEl_ &&
|
||||
['on', 'one', 'off', 'trigger'].every(k => typeof object[k] === 'function');
|
||||
|
||||
/**
|
||||
* Whether a value is a valid event type - non-empty string or array.
|
||||
*
|
||||
* @private
|
||||
* @param {string|Array} type
|
||||
* The type value to test.
|
||||
*
|
||||
* @return {boolean}
|
||||
* Whether or not the type is a valid event type.
|
||||
*/
|
||||
const isValidEventType = (type) =>
|
||||
// The regex here verifies that the `type` contains at least one non-
|
||||
// whitespace character.
|
||||
(typeof type === 'string' && (/\S/).test(type)) ||
|
||||
(Array.isArray(type) && !!type.length);
|
||||
|
||||
/**
|
||||
* Validates a value to determine if it is a valid event target. Throws if not.
|
||||
*
|
||||
* @private
|
||||
* @throws {Error}
|
||||
* If the target does not appear to be a valid event target.
|
||||
*
|
||||
* @param {Object} target
|
||||
* The object to test.
|
||||
*/
|
||||
const validateTarget = (target) => {
|
||||
if (!target.nodeName && !isEvented(target)) {
|
||||
throw new Error('Invalid target; must be a DOM node or evented object.');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates a value to determine if it is a valid event target. Throws if not.
|
||||
*
|
||||
* @private
|
||||
* @throws {Error}
|
||||
* If the type does not appear to be a valid event type.
|
||||
*
|
||||
* @param {string|Array} type
|
||||
* The type to test.
|
||||
*/
|
||||
const validateEventType = (type) => {
|
||||
if (!isValidEventType(type)) {
|
||||
throw new Error('Invalid event type; must be a non-empty string or array.');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates a value to determine if it is a valid listener. Throws if not.
|
||||
*
|
||||
* @private
|
||||
* @throws {Error}
|
||||
* If the listener is not a function.
|
||||
*
|
||||
* @param {Function} listener
|
||||
* The listener to test.
|
||||
*/
|
||||
const validateListener = (listener) => {
|
||||
if (typeof listener !== 'function') {
|
||||
throw new Error('Invalid listener; must be a function.');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes an array of arguments given to `on()` or `one()`, validates them, and
|
||||
* normalizes them into an object.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} self
|
||||
* The evented object on which `on()` or `one()` was called. This
|
||||
* object will be bound as the `this` value for the listener.
|
||||
*
|
||||
* @param {Array} args
|
||||
* An array of arguments passed to `on()` or `one()`.
|
||||
*
|
||||
* @return {Object}
|
||||
* An object containing useful values for `on()` or `one()` calls.
|
||||
*/
|
||||
const normalizeListenArgs = (self, args) => {
|
||||
|
||||
// If the number of arguments is less than 3, the target is always the
|
||||
// evented object itself.
|
||||
const isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_;
|
||||
let target;
|
||||
let type;
|
||||
let listener;
|
||||
|
||||
if (isTargetingSelf) {
|
||||
target = self.eventBusEl_;
|
||||
|
||||
// Deal with cases where we got 3 arguments, but we are still listening to
|
||||
// the evented object itself.
|
||||
if (args.length >= 3) {
|
||||
args.shift();
|
||||
}
|
||||
|
||||
[type, listener] = args;
|
||||
} else {
|
||||
[target, type, listener] = args;
|
||||
}
|
||||
|
||||
validateTarget(target);
|
||||
validateEventType(type);
|
||||
validateListener(listener);
|
||||
|
||||
listener = Fn.bind(self, listener);
|
||||
|
||||
return {isTargetingSelf, target, type, listener};
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds the listener to the event type(s) on the target, normalizing for
|
||||
* the type of target.
|
||||
*
|
||||
* @private
|
||||
* @param {Element|Object} target
|
||||
* A DOM node or evented object.
|
||||
*
|
||||
* @param {string} method
|
||||
* The event binding method to use ("on" or "one").
|
||||
*
|
||||
* @param {string|Array} type
|
||||
* One or more event type(s).
|
||||
*
|
||||
* @param {Function} listener
|
||||
* A listener function.
|
||||
*/
|
||||
const listen = (target, method, type, listener) => {
|
||||
validateTarget(target);
|
||||
|
||||
if (target.nodeName) {
|
||||
Events[method](target, type, listener);
|
||||
} else {
|
||||
target[method](type, listener);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains methods that provide event capabilites to an object which is passed
|
||||
* to {@link module:evented|evented}.
|
||||
*
|
||||
* @mixin EventedMixin
|
||||
*/
|
||||
const EventedMixin = {
|
||||
|
||||
/**
|
||||
* Add a listener to an event (or events) on this object or another evented
|
||||
* object.
|
||||
*
|
||||
* @param {string|Array|Element|Object} targetOrType
|
||||
* If this is a string or array, it represents the event type(s)
|
||||
* that will trigger the listener.
|
||||
*
|
||||
* Another evented object can be passed here instead, which will
|
||||
* cause the listener to listen for events on _that_ object.
|
||||
*
|
||||
* In either case, the listener's `this` value will be bound to
|
||||
* this object.
|
||||
*
|
||||
* @param {string|Array|Function} typeOrListener
|
||||
* If the first argument was a string or array, this should be the
|
||||
* listener function. Otherwise, this is a string or array of event
|
||||
* type(s).
|
||||
*
|
||||
* @param {Function} [listener]
|
||||
* If the first argument was another evented object, this will be
|
||||
* the listener function.
|
||||
*/
|
||||
on(...args) {
|
||||
const {isTargetingSelf, target, type, listener} = normalizeListenArgs(this, args);
|
||||
|
||||
listen(target, 'on', type, listener);
|
||||
|
||||
// If this object is listening to another evented object.
|
||||
if (!isTargetingSelf) {
|
||||
|
||||
// If this object is disposed, remove the listener.
|
||||
const removeListenerOnDispose = () => this.off(target, type, listener);
|
||||
|
||||
// Use the same function ID as the listener so we can remove it later it
|
||||
// using the ID of the original listener.
|
||||
removeListenerOnDispose.guid = listener.guid;
|
||||
|
||||
// Add a listener to the target's dispose event as well. This ensures
|
||||
// that if the target is disposed BEFORE this object, we remove the
|
||||
// removal listener that was just added. Otherwise, we create a memory leak.
|
||||
const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose);
|
||||
|
||||
// Use the same function ID as the listener so we can remove it later
|
||||
// it using the ID of the original listener.
|
||||
removeRemoverOnTargetDispose.guid = listener.guid;
|
||||
|
||||
listen(this, 'on', 'dispose', removeListenerOnDispose);
|
||||
listen(target, 'on', 'dispose', removeRemoverOnTargetDispose);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a listener to an event (or events) on this object or another evented
|
||||
* object. The listener will only be called once and then removed.
|
||||
*
|
||||
* @param {string|Array|Element|Object} targetOrType
|
||||
* If this is a string or array, it represents the event type(s)
|
||||
* that will trigger the listener.
|
||||
*
|
||||
* Another evented object can be passed here instead, which will
|
||||
* cause the listener to listen for events on _that_ object.
|
||||
*
|
||||
* In either case, the listener's `this` value will be bound to
|
||||
* this object.
|
||||
*
|
||||
* @param {string|Array|Function} typeOrListener
|
||||
* If the first argument was a string or array, this should be the
|
||||
* listener function. Otherwise, this is a string or array of event
|
||||
* type(s).
|
||||
*
|
||||
* @param {Function} [listener]
|
||||
* If the first argument was another evented object, this will be
|
||||
* the listener function.
|
||||
*/
|
||||
one(...args) {
|
||||
const {isTargetingSelf, target, type, listener} = normalizeListenArgs(this, args);
|
||||
|
||||
// Targeting this evented object.
|
||||
if (isTargetingSelf) {
|
||||
listen(target, 'one', type, listener);
|
||||
|
||||
// Targeting another evented object.
|
||||
} else {
|
||||
const wrapper = (...largs) => {
|
||||
this.off(target, type, wrapper);
|
||||
listener.apply(null, largs);
|
||||
};
|
||||
|
||||
// Use the same function ID as the listener so we can remove it later
|
||||
// it using the ID of the original listener.
|
||||
wrapper.guid = listener.guid;
|
||||
listen(target, 'one', type, wrapper);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes listener(s) from event(s) on an evented object.
|
||||
*
|
||||
* @param {string|Array|Element|Object} [targetOrType]
|
||||
* If this is a string or array, it represents the event type(s).
|
||||
*
|
||||
* Another evented object can be passed here instead, in which case
|
||||
* ALL 3 arguments are _required_.
|
||||
*
|
||||
* @param {string|Array|Function} [typeOrListener]
|
||||
* If the first argument was a string or array, this may be the
|
||||
* listener function. Otherwise, this is a string or array of event
|
||||
* type(s).
|
||||
*
|
||||
* @param {Function} [listener]
|
||||
* If the first argument was another evented object, this will be
|
||||
* the listener function; otherwise, _all_ listeners bound to the
|
||||
* event type(s) will be removed.
|
||||
*/
|
||||
off(targetOrType, typeOrListener, listener) {
|
||||
|
||||
// Targeting this evented object.
|
||||
if (!targetOrType || isValidEventType(targetOrType)) {
|
||||
Events.off(this.eventBusEl_, targetOrType, typeOrListener);
|
||||
|
||||
// Targeting another evented object.
|
||||
} else {
|
||||
const target = targetOrType;
|
||||
const type = typeOrListener;
|
||||
|
||||
// Fail fast and in a meaningful way!
|
||||
validateTarget(target);
|
||||
validateEventType(type);
|
||||
validateListener(listener);
|
||||
|
||||
// Ensure there's at least a guid, even if the function hasn't been used
|
||||
listener = Fn.bind(this, listener);
|
||||
|
||||
// Remove the dispose listener on this evented object, which was given
|
||||
// the same guid as the event listener in on().
|
||||
this.off('dispose', listener);
|
||||
|
||||
if (target.nodeName) {
|
||||
Events.off(target, type, listener);
|
||||
Events.off(target, 'dispose', listener);
|
||||
} else if (isEvented(target)) {
|
||||
target.off(type, listener);
|
||||
target.off('dispose', listener);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fire an event on this evented object, causing its listeners to be called.
|
||||
*
|
||||
* @param {string|Object} event
|
||||
* An event type or an object with a type property.
|
||||
*
|
||||
* @param {Object} [hash]
|
||||
* An additional object to pass along to listeners.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* Whether or not the default behavior was prevented.
|
||||
*/
|
||||
trigger(event, hash) {
|
||||
return Events.trigger(this.eventBusEl_, event, hash);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Applies {@link module:evented~EventedMixin|EventedMixin} to a target object.
|
||||
*
|
||||
* @param {Object} target
|
||||
* The object to which to add event methods.
|
||||
*
|
||||
* @param {Object} [options={}]
|
||||
* Options for customizing the mixin behavior.
|
||||
*
|
||||
* @param {String} [options.eventBusKey]
|
||||
* By default, adds a `eventBusEl_` DOM element to the target object,
|
||||
* which is used as an event bus. If the target object already has a
|
||||
* DOM element that should be used, pass its key here.
|
||||
*
|
||||
* @return {Object}
|
||||
* The target object.
|
||||
*/
|
||||
function evented(target, options = {}) {
|
||||
const {eventBusKey} = options;
|
||||
|
||||
// Set or create the eventBusEl_.
|
||||
if (eventBusKey) {
|
||||
if (!target[eventBusKey].nodeName) {
|
||||
throw new Error(`The eventBusKey "${eventBusKey}" does not refer to an element.`);
|
||||
}
|
||||
target.eventBusEl_ = target[eventBusKey];
|
||||
} else {
|
||||
target.eventBusEl_ = Dom.createEl('span', {className: 'vjs-event-bus'});
|
||||
}
|
||||
|
||||
Obj.assign(target, EventedMixin);
|
||||
|
||||
// When any evented object is disposed, it removes all its listeners.
|
||||
target.on('dispose', () => target.off());
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
export default evented;
|
||||
export {isEvented};
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* @file mixins/stateful.js
|
||||
* @module stateful
|
||||
*/
|
||||
import {isEvented} from './evented';
|
||||
import * as Obj from '../utils/obj';
|
||||
|
||||
/**
|
||||
* Contains methods that provide statefulness to an object which is passed
|
||||
* to {@link module:stateful}.
|
||||
*
|
||||
* @mixin StatefulMixin
|
||||
*/
|
||||
const StatefulMixin = {
|
||||
|
||||
/**
|
||||
* A hash containing arbitrary keys and values representing the state of
|
||||
* the object.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
state: {},
|
||||
|
||||
/**
|
||||
* Set the state of an object by mutating its
|
||||
* {@link module:stateful~StatefulMixin.state|state} object in place.
|
||||
*
|
||||
* @fires module:stateful~StatefulMixin#statechanged
|
||||
* @param {Object|Function} stateUpdates
|
||||
* A new set of properties to shallow-merge into the plugin state.
|
||||
* Can be a plain object or a function returning a plain object.
|
||||
*
|
||||
* @returns {Object|undefined}
|
||||
* An object containing changes that occurred. If no changes
|
||||
* occurred, returns `undefined`.
|
||||
*/
|
||||
setState(stateUpdates) {
|
||||
|
||||
// Support providing the `stateUpdates` state as a function.
|
||||
if (typeof stateUpdates === 'function') {
|
||||
stateUpdates = stateUpdates();
|
||||
}
|
||||
|
||||
let changes;
|
||||
|
||||
Obj.each(stateUpdates, (value, key) => {
|
||||
|
||||
// Record the change if the value is different from what's in the
|
||||
// current state.
|
||||
if (this.state[key] !== value) {
|
||||
changes = changes || {};
|
||||
changes[key] = {
|
||||
from: this.state[key],
|
||||
to: value
|
||||
};
|
||||
}
|
||||
|
||||
this.state[key] = value;
|
||||
});
|
||||
|
||||
// Only trigger "statechange" if there were changes AND we have a trigger
|
||||
// function. This allows us to not require that the target object be an
|
||||
// evented object.
|
||||
if (changes && isEvented(this)) {
|
||||
|
||||
/**
|
||||
* An event triggered on an object that is both
|
||||
* {@link module:stateful|stateful} and {@link module:evented|evented}
|
||||
* indicating that its state has changed.
|
||||
*
|
||||
* @event module:stateful~StatefulMixin#statechanged
|
||||
* @type {Object}
|
||||
* @property {Object} changes
|
||||
* A hash containing the properties that were changed and
|
||||
* the values they were changed `from` and `to`.
|
||||
*/
|
||||
this.trigger({
|
||||
changes,
|
||||
type: 'statechanged'
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target
|
||||
* object.
|
||||
*
|
||||
* If the target object is {@link module:evented|evented} and has a
|
||||
* `handleStateChanged` method, that method will be automatically bound to the
|
||||
* `statechanged` event on itself.
|
||||
*
|
||||
* @param {Object} target
|
||||
* The object to be made stateful.
|
||||
*
|
||||
* @param {Object} [defaultState]
|
||||
* A default set of properties to populate the newly-stateful object's
|
||||
* `state` property.
|
||||
*
|
||||
* @returns {Object}
|
||||
* Returns the `target`.
|
||||
*/
|
||||
function stateful(target, defaultState) {
|
||||
Obj.assign(target, StatefulMixin);
|
||||
|
||||
// This happens after the mixing-in because we need to replace the `state`
|
||||
// added in that step.
|
||||
target.state = Obj.assign({}, target.state, defaultState);
|
||||
|
||||
// Auto-bind the `handleStateChanged` method of the target object if it exists.
|
||||
if (typeof target.handleStateChanged === 'function' && isEvented(target)) {
|
||||
target.on('statechanged', target.handleStateChanged);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
export default stateful;
|
||||
+49
-68
@@ -153,20 +153,17 @@ class ModalDialog extends Component {
|
||||
*
|
||||
* @fires ModalDialog#beforemodalopen
|
||||
* @fires ModalDialog#modalopen
|
||||
*
|
||||
* @return {ModalDialog}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
open() {
|
||||
if (!this.opened_) {
|
||||
const player = this.player();
|
||||
|
||||
/**
|
||||
* Fired just before a `ModalDialog` is opened.
|
||||
*
|
||||
* @event ModalDialog#beforemodalopen
|
||||
* @type {EventTarget~Event}
|
||||
*/
|
||||
* Fired just before a `ModalDialog` is opened.
|
||||
*
|
||||
* @event ModalDialog#beforemodalopen
|
||||
* @type {EventTarget~Event}
|
||||
*/
|
||||
this.trigger('beforemodalopen');
|
||||
this.opened_ = true;
|
||||
|
||||
@@ -193,15 +190,14 @@ class ModalDialog extends Component {
|
||||
this.el().setAttribute('aria-hidden', 'false');
|
||||
|
||||
/**
|
||||
* Fired just after a `ModalDialog` is opened.
|
||||
*
|
||||
* @event ModalDialog#modalopen
|
||||
* @type {EventTarget~Event}
|
||||
*/
|
||||
* Fired just after a `ModalDialog` is opened.
|
||||
*
|
||||
* @event ModalDialog#modalopen
|
||||
* @type {EventTarget~Event}
|
||||
*/
|
||||
this.trigger('modalopen');
|
||||
this.hasBeenOpened_ = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,48 +222,45 @@ class ModalDialog extends Component {
|
||||
*
|
||||
* @fires ModalDialog#beforemodalclose
|
||||
* @fires ModalDialog#modalclose
|
||||
*
|
||||
* @return {ModalDialog}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
close() {
|
||||
if (this.opened_) {
|
||||
const player = this.player();
|
||||
|
||||
/**
|
||||
* Fired just before a `ModalDialog` is closed.
|
||||
*
|
||||
* @event ModalDialog#beforemodalclose
|
||||
* @type {EventTarget~Event}
|
||||
*/
|
||||
this.trigger('beforemodalclose');
|
||||
this.opened_ = false;
|
||||
|
||||
if (this.wasPlaying_) {
|
||||
player.play();
|
||||
}
|
||||
|
||||
if (this.closeable()) {
|
||||
this.off(this.el_.ownerDocument, 'keydown', Fn.bind(this, this.handleKeyPress));
|
||||
}
|
||||
|
||||
player.controls(true);
|
||||
this.hide();
|
||||
this.el().setAttribute('aria-hidden', 'true');
|
||||
|
||||
/**
|
||||
* Fired just after a `ModalDialog` is closed.
|
||||
*
|
||||
* @event ModalDialog#modalclose
|
||||
* @type {EventTarget~Event}
|
||||
*/
|
||||
this.trigger('modalclose');
|
||||
|
||||
if (this.options_.temporary) {
|
||||
this.dispose();
|
||||
}
|
||||
if (!this.opened_) {
|
||||
return;
|
||||
}
|
||||
const player = this.player();
|
||||
|
||||
/**
|
||||
* Fired just before a `ModalDialog` is closed.
|
||||
*
|
||||
* @event ModalDialog#beforemodalclose
|
||||
* @type {EventTarget~Event}
|
||||
*/
|
||||
this.trigger('beforemodalclose');
|
||||
this.opened_ = false;
|
||||
|
||||
if (this.wasPlaying_) {
|
||||
player.play();
|
||||
}
|
||||
|
||||
if (this.closeable()) {
|
||||
this.off(this.el_.ownerDocument, 'keydown', Fn.bind(this, this.handleKeyPress));
|
||||
}
|
||||
|
||||
player.controls(true);
|
||||
this.hide();
|
||||
this.el().setAttribute('aria-hidden', 'true');
|
||||
|
||||
/**
|
||||
* Fired just after a `ModalDialog` is closed.
|
||||
*
|
||||
* @event ModalDialog#modalclose
|
||||
* @type {EventTarget~Event}
|
||||
*/
|
||||
this.trigger('modalclose');
|
||||
|
||||
if (this.options_.temporary) {
|
||||
this.dispose();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -310,12 +303,9 @@ class ModalDialog extends Component {
|
||||
/**
|
||||
* Fill the modal's content element with the modal's "content" option.
|
||||
* The content element will be emptied before this change takes place.
|
||||
*
|
||||
* @return {ModalDialog}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
fill() {
|
||||
return this.fillWith(this.content());
|
||||
this.fillWith(this.content());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,11 +315,8 @@ class ModalDialog extends Component {
|
||||
* @fires ModalDialog#beforemodalfill
|
||||
* @fires ModalDialog#modalfill
|
||||
*
|
||||
* @param {Mixed} [content]
|
||||
* The same rules apply to this as apply to the `content` option.
|
||||
*
|
||||
* @return {ModalDialog}
|
||||
* Returns itself; method can be chained.
|
||||
* @param {Mixed} [content]
|
||||
* The same rules apply to this as apply to the `content` option.
|
||||
*/
|
||||
fillWith(content) {
|
||||
const contentEl = this.contentEl();
|
||||
@@ -364,8 +351,6 @@ class ModalDialog extends Component {
|
||||
} else {
|
||||
parentEl.appendChild(contentEl);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -373,9 +358,6 @@ class ModalDialog extends Component {
|
||||
*
|
||||
* @fires ModalDialog#beforemodalempty
|
||||
* @fires ModalDialog#modalempty
|
||||
*
|
||||
* @return {ModalDialog}
|
||||
* Returns itself; method can be chained.
|
||||
*/
|
||||
empty() {
|
||||
/**
|
||||
@@ -394,7 +376,6 @@ class ModalDialog extends Component {
|
||||
* @type {EventTarget~Event}
|
||||
*/
|
||||
this.trigger('modalempty');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+422
-342
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
@@ -0,0 +1,444 @@
|
||||
/**
|
||||
* @file plugin.js
|
||||
*/
|
||||
import evented from './mixins/evented';
|
||||
import stateful from './mixins/stateful';
|
||||
import * as Events from './utils/events';
|
||||
import * as Fn from './utils/fn';
|
||||
import Player from './player';
|
||||
|
||||
/**
|
||||
* The base plugin name.
|
||||
*
|
||||
* @private
|
||||
* @constant
|
||||
* @type {string}
|
||||
*/
|
||||
const BASE_PLUGIN_NAME = 'plugin';
|
||||
|
||||
/**
|
||||
* The key on which a player's active plugins cache is stored.
|
||||
*
|
||||
* @private
|
||||
* @constant
|
||||
* @type {string}
|
||||
*/
|
||||
const PLUGIN_CACHE_KEY = 'activePlugins_';
|
||||
|
||||
/**
|
||||
* Stores registered plugins in a private space.
|
||||
*
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
const pluginStorage = {};
|
||||
|
||||
/**
|
||||
* Reports whether or not a plugin has been registered.
|
||||
*
|
||||
* @private
|
||||
* @param {string} name
|
||||
* The name of a plugin.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* Whether or not the plugin has been registered.
|
||||
*/
|
||||
const pluginExists = (name) => pluginStorage.hasOwnProperty(name);
|
||||
|
||||
/**
|
||||
* Get a single registered plugin by name.
|
||||
*
|
||||
* @private
|
||||
* @param {string} name
|
||||
* The name of a plugin.
|
||||
*
|
||||
* @returns {Function|undefined}
|
||||
* The plugin (or undefined).
|
||||
*/
|
||||
const getPlugin = (name) => pluginExists(name) ? pluginStorage[name] : undefined;
|
||||
|
||||
/**
|
||||
* Marks a plugin as "active" on a player.
|
||||
*
|
||||
* Also, ensures that the player has an object for tracking active plugins.
|
||||
*
|
||||
* @private
|
||||
* @param {Player} player
|
||||
* A Video.js player instance.
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of a plugin.
|
||||
*/
|
||||
const markPluginAsActive = (player, name) => {
|
||||
player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {};
|
||||
player[PLUGIN_CACHE_KEY][name] = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a basic plugin function and returns a wrapper function which marks
|
||||
* on the player that the plugin has been activated.
|
||||
*
|
||||
* @private
|
||||
* @param {string} name
|
||||
* The name of the plugin.
|
||||
*
|
||||
* @param {Function} plugin
|
||||
* The basic plugin.
|
||||
*
|
||||
* @returns {Function}
|
||||
* A wrapper function for the given plugin.
|
||||
*/
|
||||
const createBasicPlugin = (name, plugin) => function() {
|
||||
const instance = plugin.apply(this, arguments);
|
||||
|
||||
markPluginAsActive(this, name);
|
||||
|
||||
// We trigger the "pluginsetup" event on the player regardless, but we want
|
||||
// the hash to be consistent with the hash provided for advanced plugins.
|
||||
// The only potentially counter-intuitive thing here is the `instance` is the
|
||||
// value returned by the `plugin` function.
|
||||
this.trigger('pluginsetup', {name, plugin, instance});
|
||||
return instance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a plugin sub-class and returns a factory function for generating
|
||||
* instances of it.
|
||||
*
|
||||
* This factory function will replace itself with an instance of the requested
|
||||
* sub-class of Plugin.
|
||||
*
|
||||
* @private
|
||||
* @param {string} name
|
||||
* The name of the plugin.
|
||||
*
|
||||
* @param {Plugin} PluginSubClass
|
||||
* The advanced plugin.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
const createPluginFactory = (name, PluginSubClass) => {
|
||||
|
||||
// Add a `name` property to the plugin prototype so that each plugin can
|
||||
// refer to itself by name.
|
||||
PluginSubClass.prototype.name = name;
|
||||
|
||||
return function(...args) {
|
||||
const instance = new PluginSubClass(...[this, ...args]);
|
||||
|
||||
// The plugin is replaced by a function that returns the current instance.
|
||||
this[name] = () => instance;
|
||||
|
||||
return instance;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Parent class for all advanced plugins.
|
||||
*
|
||||
* @mixes module:evented~EventedMixin
|
||||
* @mixes module:stateful~StatefulMixin
|
||||
* @fires Player#pluginsetup
|
||||
* @listens Player#dispose
|
||||
* @throws {Error}
|
||||
* If attempting to instantiate the base {@link Plugin} class
|
||||
* directly instead of via a sub-class.
|
||||
*/
|
||||
class Plugin {
|
||||
|
||||
/**
|
||||
* Creates an instance of this class.
|
||||
*
|
||||
* Sub-classes should call `super` to ensure plugins are properly initialized.
|
||||
*
|
||||
* @param {Player} player
|
||||
* A Video.js player instance.
|
||||
*/
|
||||
constructor(player) {
|
||||
this.player = player;
|
||||
|
||||
if (this.constructor === Plugin) {
|
||||
throw new Error('Plugin must be sub-classed; not directly instantiated.');
|
||||
}
|
||||
|
||||
// Make this object evented, but remove the added `trigger` method so we
|
||||
// use the prototype version instead.
|
||||
evented(this);
|
||||
delete this.trigger;
|
||||
|
||||
stateful(this, this.constructor.defaultState);
|
||||
markPluginAsActive(player, this.name);
|
||||
|
||||
// Auto-bind the dispose method so we can use it as a listener and unbind
|
||||
// it later easily.
|
||||
this.dispose = Fn.bind(this, this.dispose);
|
||||
|
||||
// If the player is disposed, dispose the plugin.
|
||||
player.on('dispose', this.dispose);
|
||||
player.trigger('pluginsetup', this.getEventHash());
|
||||
}
|
||||
|
||||
/**
|
||||
* Each event triggered by plugins includes a hash of additional data with
|
||||
* conventional properties.
|
||||
*
|
||||
* This returns that object or mutates an existing hash.
|
||||
*
|
||||
* @param {Object} [hash={}]
|
||||
* An object to be used as event an event hash.
|
||||
*
|
||||
* @returns {Plugin~PluginEventHash}
|
||||
* An event hash object with provided properties mixed-in.
|
||||
*/
|
||||
getEventHash(hash = {}) {
|
||||
hash.name = this.name;
|
||||
hash.plugin = this.constructor;
|
||||
hash.instance = this;
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an event on the plugin object and overrides
|
||||
* {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}.
|
||||
*
|
||||
* @param {string|Object} event
|
||||
* An event type or an object with a type property.
|
||||
*
|
||||
* @param {Object} [hash={}]
|
||||
* Additional data hash to merge with a
|
||||
* {@link Plugin~PluginEventHash|PluginEventHash}.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* Whether or not default was prevented.
|
||||
*/
|
||||
trigger(event, hash = {}) {
|
||||
return Events.trigger(this.eventBusEl_, event, this.getEventHash(hash));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles "statechanged" events on the plugin. No-op by default, override by
|
||||
* subclassing.
|
||||
*
|
||||
* @abstract
|
||||
* @param {Event} e
|
||||
* An event object provided by a "statechanged" event.
|
||||
*
|
||||
* @param {Object} e.changes
|
||||
* An object describing changes that occurred with the "statechanged"
|
||||
* event.
|
||||
*/
|
||||
handleStateChanged(e) {}
|
||||
|
||||
/**
|
||||
* Disposes a plugin.
|
||||
*
|
||||
* Subclasses can override this if they want, but for the sake of safety,
|
||||
* it's probably best to subscribe the "dispose" event.
|
||||
*
|
||||
* @fires Plugin#dispose
|
||||
*/
|
||||
dispose() {
|
||||
const {name, player} = this;
|
||||
|
||||
/**
|
||||
* Signals that a advanced plugin is about to be disposed.
|
||||
*
|
||||
* @event Plugin#dispose
|
||||
* @type {EventTarget~Event}
|
||||
*/
|
||||
this.trigger('dispose');
|
||||
this.off();
|
||||
player.off('dispose', this.dispose);
|
||||
|
||||
// Eliminate any possible sources of leaking memory by clearing up
|
||||
// references between the player and the plugin instance and nulling out
|
||||
// the plugin's state and replacing methods with a function that throws.
|
||||
player[PLUGIN_CACHE_KEY][name] = false;
|
||||
this.player = this.state = null;
|
||||
|
||||
// Finally, replace the plugin name on the player with a new factory
|
||||
// function, so that the plugin is ready to be set up again.
|
||||
player[name] = createPluginFactory(name, pluginStorage[name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`).
|
||||
*
|
||||
* @param {string|Function} plugin
|
||||
* If a string, matches the name of a plugin. If a function, will be
|
||||
* tested directly.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* Whether or not a plugin is a basic plugin.
|
||||
*/
|
||||
static isBasic(plugin) {
|
||||
const p = (typeof plugin === 'string') ? getPlugin(plugin) : plugin;
|
||||
|
||||
return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a Video.js plugin.
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of the plugin to be registered. Must be a string and
|
||||
* must not match an existing plugin or a method on the `Player`
|
||||
* prototype.
|
||||
*
|
||||
* @param {Function} plugin
|
||||
* A sub-class of `Plugin` or a function for basic plugins.
|
||||
*
|
||||
* @returns {Function}
|
||||
* For advanced plugins, a factory function for that plugin. For
|
||||
* basic plugins, a wrapper function that initializes the plugin.
|
||||
*/
|
||||
static registerPlugin(name, plugin) {
|
||||
if (typeof name !== 'string') {
|
||||
throw new Error(`Illegal plugin name, "${name}", must be a string, was ${typeof name}.`);
|
||||
}
|
||||
|
||||
if (pluginExists(name) || Player.prototype.hasOwnProperty(name)) {
|
||||
throw new Error(`Illegal plugin name, "${name}", already exists.`);
|
||||
}
|
||||
|
||||
if (typeof plugin !== 'function') {
|
||||
throw new Error(`Illegal plugin for "${name}", must be a function, was ${typeof plugin}.`);
|
||||
}
|
||||
|
||||
pluginStorage[name] = plugin;
|
||||
|
||||
// Add a player prototype method for all sub-classed plugins (but not for
|
||||
// the base Plugin class).
|
||||
if (name !== BASE_PLUGIN_NAME) {
|
||||
if (Plugin.isBasic(plugin)) {
|
||||
Player.prototype[name] = createBasicPlugin(name, plugin);
|
||||
} else {
|
||||
Player.prototype[name] = createPluginFactory(name, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* De-register a Video.js plugin.
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of the plugin to be deregistered.
|
||||
*/
|
||||
static deregisterPlugin(name) {
|
||||
if (name === BASE_PLUGIN_NAME) {
|
||||
throw new Error('Cannot de-register base plugin.');
|
||||
}
|
||||
if (pluginExists(name)) {
|
||||
delete pluginStorage[name];
|
||||
delete Player.prototype[name];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an object containing multiple Video.js plugins.
|
||||
*
|
||||
* @param {Array} [names]
|
||||
* If provided, should be an array of plugin names. Defaults to _all_
|
||||
* plugin names.
|
||||
*
|
||||
* @returns {Object|undefined}
|
||||
* An object containing plugin(s) associated with their name(s) or
|
||||
* `undefined` if no matching plugins exist).
|
||||
*/
|
||||
static getPlugins(names = Object.keys(pluginStorage)) {
|
||||
let result;
|
||||
|
||||
names.forEach(name => {
|
||||
const plugin = getPlugin(name);
|
||||
|
||||
if (plugin) {
|
||||
result = result || {};
|
||||
result[name] = plugin;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a plugin's version, if available
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of a plugin.
|
||||
*
|
||||
* @returns {string}
|
||||
* The plugin's version or an empty string.
|
||||
*/
|
||||
static getPluginVersion(name) {
|
||||
const plugin = getPlugin(name);
|
||||
|
||||
return plugin && plugin.VERSION || '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a plugin by name if it exists.
|
||||
*
|
||||
* @static
|
||||
* @method getPlugin
|
||||
* @memberOf Plugin
|
||||
* @param {string} name
|
||||
* The name of a plugin.
|
||||
*
|
||||
* @returns {Function|undefined}
|
||||
* The plugin (or `undefined`).
|
||||
*/
|
||||
Plugin.getPlugin = getPlugin;
|
||||
|
||||
/**
|
||||
* The name of the base plugin class as it is registered.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME;
|
||||
|
||||
Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin);
|
||||
|
||||
/**
|
||||
* Documented in player.js
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
Player.prototype.usingPlugin = function(name) {
|
||||
return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Documented in player.js
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
Player.prototype.hasPlugin = function(name) {
|
||||
return !!pluginExists(name);
|
||||
};
|
||||
|
||||
export default Plugin;
|
||||
|
||||
/**
|
||||
* Signals that a plugin has just been set up on a player.
|
||||
*
|
||||
* @event Player#pluginsetup
|
||||
* @type {Plugin~PluginEventHash}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Plugin~PluginEventHash
|
||||
*
|
||||
* @property {string} instance
|
||||
* For basic plugins, the return value of the plugin function. For
|
||||
* advanced plugins, the plugin instance on which the event is fired.
|
||||
*
|
||||
* @property {string} name
|
||||
* The name of the plugin.
|
||||
*
|
||||
* @property {string} plugin
|
||||
* For basic plugins, the plugin function. For advanced plugins, the
|
||||
* plugin class/constructor.
|
||||
*/
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* @file plugins.js
|
||||
* @module plugins
|
||||
*/
|
||||
import Player from './player.js';
|
||||
|
||||
/**
|
||||
* The method for registering a video.js plugin. {@link videojs:videojs.registerPlugin].
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of the plugin that is being registered
|
||||
*
|
||||
* @param {plugins:PluginFn} init
|
||||
* The function that gets run when a `Player` initializes.
|
||||
*/
|
||||
const plugin = function(name, init) {
|
||||
Player.prototype[name] = init;
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
+22
-12
@@ -38,7 +38,10 @@ class Slider extends Component {
|
||||
this.on('click', this.handleClick);
|
||||
|
||||
this.on(player, 'controlsvisible', this.update);
|
||||
this.on(player, this.playerEvent, this.update);
|
||||
|
||||
if (this.playerEvent) {
|
||||
this.on(player, this.playerEvent, this.update);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,16 +159,23 @@ class Slider extends Component {
|
||||
|
||||
/**
|
||||
* Update the progress bar of the `Slider`.
|
||||
*
|
||||
* @returns {number}
|
||||
* The percentage of progress the progress bar represents as a
|
||||
* number from 0 to 1.
|
||||
*/
|
||||
update() {
|
||||
// In VolumeBar init we have a setTimeout for update that pops and update to the end of the
|
||||
// execution stack. The player is destroyed before then update will cause an error
|
||||
|
||||
// In VolumeBar init we have a setTimeout for update that pops and update
|
||||
// to the end of the execution stack. The player is destroyed before then
|
||||
// update will cause an error
|
||||
if (!this.el_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If scrubbing, we could use a cached value to make the handle keep up with the user's mouse.
|
||||
// On HTML5 browsers scrubbing is really smooth, but some flash players are slow, so we might want to utilize this later.
|
||||
// If scrubbing, we could use a cached value to make the handle keep up
|
||||
// with the user's mouse. On HTML5 browsers scrubbing is really smooth, but
|
||||
// some flash players are slow, so we might want to utilize this later.
|
||||
// var progress = (this.player_.scrubbing()) ? this.player_.getCache().currentTime / this.player_.duration() : this.player_.currentTime() / this.player_.duration();
|
||||
let progress = this.getPercent();
|
||||
const bar = this.bar;
|
||||
@@ -185,13 +195,16 @@ class Slider extends Component {
|
||||
|
||||
// Convert to a percentage for setting
|
||||
const percentage = (progress * 100).toFixed(2) + '%';
|
||||
const style = bar.el().style;
|
||||
|
||||
// Set the new bar width or height
|
||||
if (this.vertical()) {
|
||||
bar.el().style.height = percentage;
|
||||
style.height = percentage;
|
||||
} else {
|
||||
bar.el().style.width = percentage;
|
||||
style.width = percentage;
|
||||
}
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -281,10 +294,9 @@ class Slider extends Component {
|
||||
* - true if slider is vertical,
|
||||
* - false is horizontal
|
||||
*
|
||||
* @return {boolean|Slider}
|
||||
* @return {boolean}
|
||||
* - true if slider is vertical, and getting
|
||||
* - false is horizontal, and getting
|
||||
* - a reference to this object when setting
|
||||
* - false if the slider is horizontal, and getting
|
||||
*/
|
||||
vertical(bool) {
|
||||
if (bool === undefined) {
|
||||
@@ -298,8 +310,6 @@ class Slider extends Component {
|
||||
} else {
|
||||
this.addClass('vjs-slider-horizontal');
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
/**
|
||||
* @file flash-rtmp.js
|
||||
* @module flash-rtmp
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add RTMP properties to the {@link Flash} Tech.
|
||||
*
|
||||
* @param {Flash} Flash
|
||||
* The flash tech class.
|
||||
*
|
||||
* @mixin FlashRtmpDecorator
|
||||
*/
|
||||
function FlashRtmpDecorator(Flash) {
|
||||
Flash.streamingFormats = {
|
||||
'rtmp/mp4': 'MP4',
|
||||
'rtmp/flv': 'FLV'
|
||||
};
|
||||
|
||||
/**
|
||||
* Join connection and stream with an ampersand.
|
||||
*
|
||||
* @param {string} connection
|
||||
* The connection string.
|
||||
*
|
||||
* @param {string} stream
|
||||
* The stream string.
|
||||
*/
|
||||
Flash.streamFromParts = function(connection, stream) {
|
||||
return connection + '&' + stream;
|
||||
};
|
||||
|
||||
/**
|
||||
* The flash parts object that contains connection and stream info.
|
||||
*
|
||||
* @typedef {Object} Flash~PartsObject
|
||||
*
|
||||
* @property {string} connection
|
||||
* The connection string of a source, defaults to an empty string.
|
||||
*
|
||||
* @property {string} stream
|
||||
* The stream string of the source, defaults to an empty string.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert a source url into a stream and connection parts.
|
||||
*
|
||||
* @param {string} src
|
||||
* the source url
|
||||
*
|
||||
* @return {Flash~PartsObject}
|
||||
* The parts object that contains a connection and a stream
|
||||
*/
|
||||
Flash.streamToParts = function(src) {
|
||||
const parts = {
|
||||
connection: '',
|
||||
stream: ''
|
||||
};
|
||||
|
||||
if (!src) {
|
||||
return parts;
|
||||
}
|
||||
|
||||
// Look for the normal URL separator we expect, '&'.
|
||||
// If found, we split the URL into two pieces around the
|
||||
// first '&'.
|
||||
let connEnd = src.search(/&(?!\w+=)/);
|
||||
let streamBegin;
|
||||
|
||||
if (connEnd !== -1) {
|
||||
streamBegin = connEnd + 1;
|
||||
} else {
|
||||
// If there's not a '&', we use the last '/' as the delimiter.
|
||||
connEnd = streamBegin = src.lastIndexOf('/') + 1;
|
||||
if (connEnd === 0) {
|
||||
// really, there's not a '/'?
|
||||
connEnd = streamBegin = src.length;
|
||||
}
|
||||
}
|
||||
|
||||
parts.connection = src.substring(0, connEnd);
|
||||
parts.stream = src.substring(streamBegin, src.length);
|
||||
|
||||
return parts;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the source type is a streaming type.
|
||||
*
|
||||
* @param {string} srcType
|
||||
* The mime type to check.
|
||||
*
|
||||
* @return {boolean}
|
||||
* - True if the source type is a streaming type.
|
||||
* - False if the source type is not a streaming type.
|
||||
*/
|
||||
Flash.isStreamingType = function(srcType) {
|
||||
return srcType in Flash.streamingFormats;
|
||||
};
|
||||
|
||||
// RTMP has four variations, any string starting
|
||||
// with one of these protocols should be valid
|
||||
|
||||
/**
|
||||
* Regular expression used to check if the source is an rtmp source.
|
||||
*
|
||||
* @property {RegExp} Flash.RTMP_RE
|
||||
*/
|
||||
Flash.RTMP_RE = /^rtmp[set]?:\/\//i;
|
||||
|
||||
/**
|
||||
* Check if the source itself is a streaming type.
|
||||
*
|
||||
* @param {string} src
|
||||
* The url to the source.
|
||||
*
|
||||
* @return {boolean}
|
||||
* - True if the source url indicates that the source is streaming.
|
||||
* - False if the shource url indicates that the source url is not streaming.
|
||||
*/
|
||||
Flash.isStreamingSrc = function(src) {
|
||||
return Flash.RTMP_RE.test(src);
|
||||
};
|
||||
|
||||
/**
|
||||
* A source handler for RTMP urls
|
||||
* @type {Object}
|
||||
*/
|
||||
Flash.rtmpSourceHandler = {};
|
||||
|
||||
/**
|
||||
* Check if Flash can play the given mime type.
|
||||
*
|
||||
* @param {string} type
|
||||
* The mime type to check
|
||||
*
|
||||
* @return {string}
|
||||
* 'maybe', or '' (empty string)
|
||||
*/
|
||||
Flash.rtmpSourceHandler.canPlayType = function(type) {
|
||||
if (Flash.isStreamingType(type)) {
|
||||
return 'maybe';
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if Flash can handle the source natively
|
||||
*
|
||||
* @param {Object} source
|
||||
* The source object
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* The options passed to the tech
|
||||
*
|
||||
* @return {string}
|
||||
* 'maybe', or '' (empty string)
|
||||
*/
|
||||
Flash.rtmpSourceHandler.canHandleSource = function(source, options) {
|
||||
const can = Flash.rtmpSourceHandler.canPlayType(source.type);
|
||||
|
||||
if (can) {
|
||||
return can;
|
||||
}
|
||||
|
||||
if (Flash.isStreamingSrc(source.src)) {
|
||||
return 'maybe';
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Pass the source to the flash object.
|
||||
*
|
||||
* @param {Object} source
|
||||
* The source object
|
||||
*
|
||||
* @param {Flash} tech
|
||||
* The instance of the Flash tech
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* The options to pass to the source
|
||||
*/
|
||||
Flash.rtmpSourceHandler.handleSource = function(source, tech, options) {
|
||||
const srcParts = Flash.streamToParts(source.src);
|
||||
|
||||
tech.setRtmpConnection(srcParts.connection);
|
||||
tech.setRtmpStream(srcParts.stream);
|
||||
};
|
||||
|
||||
// Register the native source handler
|
||||
Flash.registerSourceHandler(Flash.rtmpSourceHandler);
|
||||
|
||||
return Flash;
|
||||
}
|
||||
|
||||
export default FlashRtmpDecorator;
|
||||
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+179
-198
@@ -2,10 +2,8 @@
|
||||
* @file html5.js
|
||||
*/
|
||||
import Tech from './tech.js';
|
||||
import Component from '../component';
|
||||
import * as Dom from '../utils/dom.js';
|
||||
import * as Url from '../utils/url.js';
|
||||
import * as Fn from '../utils/fn.js';
|
||||
import log from '../utils/log.js';
|
||||
import tsml from 'tsml';
|
||||
import * as browser from '../utils/browser.js';
|
||||
@@ -14,6 +12,7 @@ import window from 'global/window';
|
||||
import {assign} from '../utils/obj';
|
||||
import mergeOptions from '../utils/merge-options.js';
|
||||
import toTitleCase from '../utils/to-title-case.js';
|
||||
import {NORMAL as TRACK_TYPES} from '../tracks/track-types';
|
||||
|
||||
/**
|
||||
* HTML5 Media Controller - Wrapper for HTML5 Media API
|
||||
@@ -68,7 +67,7 @@ class Html5 extends Tech {
|
||||
} else {
|
||||
// store HTMLTrackElement and TextTrack to remote list
|
||||
this.remoteTextTrackEls().addTrackElement_(node);
|
||||
this.remoteTextTracks().addTrack_(node.track);
|
||||
this.remoteTextTracks().addTrack(node.track);
|
||||
if (!crossoriginTracks &&
|
||||
!this.el_.hasAttribute('crossorigin') &&
|
||||
Url.isCrossOrigin(node.src)) {
|
||||
@@ -83,52 +82,10 @@ class Html5 extends Tech {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add text tracks into this list
|
||||
const trackTypes = ['audio', 'video'];
|
||||
|
||||
// ProxyNative Video/Audio Track
|
||||
trackTypes.forEach((type) => {
|
||||
const elTracks = this.el()[`${type}Tracks`];
|
||||
const techTracks = this[`${type}Tracks`]();
|
||||
const capitalType = toTitleCase(type);
|
||||
|
||||
if (!this[`featuresNative${capitalType}Tracks`] ||
|
||||
!elTracks ||
|
||||
!elTracks.addEventListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
this[`handle${capitalType}TrackChange_`] = (e) => {
|
||||
techTracks.trigger({
|
||||
type: 'change',
|
||||
target: techTracks,
|
||||
currentTarget: techTracks,
|
||||
srcElement: techTracks
|
||||
});
|
||||
};
|
||||
|
||||
this[`handle${capitalType}TrackAdd_`] = (e) => techTracks.addTrack(e.track);
|
||||
this[`handle${capitalType}TrackRemove_`] = (e) => techTracks.removeTrack(e.track);
|
||||
|
||||
elTracks.addEventListener('change', this[`handle${capitalType}TrackChange_`]);
|
||||
elTracks.addEventListener('addtrack', this[`handle${capitalType}TrackAdd_`]);
|
||||
elTracks.addEventListener('removetrack', this[`handle${capitalType}TrackRemove_`]);
|
||||
this[`removeOld${capitalType}Tracks_`] = (e) => this.removeOldTracks_(techTracks, elTracks);
|
||||
|
||||
// Remove (native) tracks that are not used anymore
|
||||
this.on('loadstart', this[`removeOld${capitalType}Tracks_`]);
|
||||
});
|
||||
|
||||
if (this.featuresNativeTextTracks) {
|
||||
if (crossoriginTracks) {
|
||||
log.warn(tsml`Text Tracks are being loaded from another origin but the crossorigin attribute isn't used.
|
||||
this.proxyNativeTracks_();
|
||||
if (this.featuresNativeTextTracks && crossoriginTracks) {
|
||||
log.warn(tsml`Text Tracks are being loaded from another origin but the crossorigin attribute isn't used.
|
||||
This may prevent text tracks from loading.`);
|
||||
}
|
||||
|
||||
this.handleTextTrackChange_ = Fn.bind(this, this.handleTextTrackChange);
|
||||
this.handleTextTrackAdd_ = Fn.bind(this, this.handleTextTrackAdd);
|
||||
this.handleTextTrackRemove_ = Fn.bind(this, this.handleTextTrackRemove);
|
||||
this.proxyNativeTextTracks_();
|
||||
}
|
||||
|
||||
// Determine if native controls should be used
|
||||
@@ -151,28 +108,81 @@ class Html5 extends Tech {
|
||||
* Dispose of `HTML5` media element and remove all tracks.
|
||||
*/
|
||||
dispose() {
|
||||
// Un-ProxyNativeTracks
|
||||
['audio', 'video', 'text'].forEach((type) => {
|
||||
const capitalType = toTitleCase(type);
|
||||
const tl = this.el_[`${type}Tracks`];
|
||||
|
||||
if (tl && tl.removeEventListener) {
|
||||
tl.removeEventListener('change', this[`handle${capitalType}TrackChange_`]);
|
||||
tl.removeEventListener('addtrack', this[`handle${capitalType}TrackAdd_`]);
|
||||
tl.removeEventListener('removetrack', this[`handle${capitalType}TrackRemove_`]);
|
||||
}
|
||||
|
||||
// Stop removing old text tracks
|
||||
if (tl) {
|
||||
this.off('loadstart', this[`removeOld${capitalType}Tracks_`]);
|
||||
}
|
||||
});
|
||||
|
||||
Html5.disposeMediaElement(this.el_);
|
||||
// tech will handle clearing of the emulated track list
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy all native track list events to our track lists if the browser we are playing
|
||||
* in supports that type of track list.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
proxyNativeTracks_() {
|
||||
TRACK_TYPES.names.forEach((name) => {
|
||||
const props = TRACK_TYPES[name];
|
||||
const elTracks = this.el()[props.getterName];
|
||||
const techTracks = this[props.getterName]();
|
||||
|
||||
if (!this[`featuresNative${props.capitalName}Tracks`] ||
|
||||
!elTracks ||
|
||||
!elTracks.addEventListener) {
|
||||
return;
|
||||
}
|
||||
const listeners = {
|
||||
change(e) {
|
||||
techTracks.trigger({
|
||||
type: 'change',
|
||||
target: techTracks,
|
||||
currentTarget: techTracks,
|
||||
srcElement: techTracks
|
||||
});
|
||||
},
|
||||
addtrack(e) {
|
||||
techTracks.addTrack(e.track);
|
||||
},
|
||||
removetrack(e) {
|
||||
techTracks.removeTrack(e.track);
|
||||
}
|
||||
};
|
||||
const removeOldTracks = function() {
|
||||
const removeTracks = [];
|
||||
|
||||
for (let i = 0; i < techTracks.length; i++) {
|
||||
let found = false;
|
||||
|
||||
for (let j = 0; j < elTracks.length; j++) {
|
||||
if (elTracks[j] === techTracks[i]) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
removeTracks.push(techTracks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
while (removeTracks.length) {
|
||||
techTracks.removeTrack(removeTracks.shift());
|
||||
}
|
||||
};
|
||||
|
||||
Object.keys(listeners).forEach((eventName) => {
|
||||
const listener = listeners[eventName];
|
||||
|
||||
elTracks.addEventListener(eventName, listener);
|
||||
this.on('dispose', (e) => elTracks.removeEventListener(eventName, listener));
|
||||
});
|
||||
|
||||
// Remove (native) tracks that are not used anymore
|
||||
this.on('loadstart', removeOldTracks);
|
||||
this.on('dispose', (e) => this.off('loadstart', removeOldTracks));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the `Html5` Tech's DOM element.
|
||||
*
|
||||
@@ -204,14 +214,14 @@ class Html5 extends Tech {
|
||||
el = document.createElement('video');
|
||||
|
||||
// determine if native controls should be used
|
||||
const tagAttributes = this.options_.tag && Dom.getElAttributes(this.options_.tag);
|
||||
const tagAttributes = this.options_.tag && Dom.getAttributes(this.options_.tag);
|
||||
const attributes = mergeOptions({}, tagAttributes);
|
||||
|
||||
if (!browser.TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) {
|
||||
delete attributes.controls;
|
||||
}
|
||||
|
||||
Dom.setElAttributes(el,
|
||||
Dom.setAttributes(el,
|
||||
assign(attributes, {
|
||||
id: this.options_.techId,
|
||||
class: 'vjs-tech'
|
||||
@@ -232,7 +242,7 @@ class Html5 extends Tech {
|
||||
if (typeof this.options_[attr] !== 'undefined') {
|
||||
overwriteAttrs[attr] = this.options_[attr];
|
||||
}
|
||||
Dom.setElAttributes(el, overwriteAttrs);
|
||||
Dom.setAttributes(el, overwriteAttrs);
|
||||
}
|
||||
|
||||
return el;
|
||||
@@ -331,131 +341,6 @@ class Html5 extends Tech {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event listeners to native text track events. This adds the native text tracks
|
||||
* to our emulated {@link TextTrackList}.
|
||||
*/
|
||||
proxyNativeTextTracks_() {
|
||||
const tt = this.el().textTracks;
|
||||
|
||||
if (tt) {
|
||||
// Add tracks - if player is initialised after DOM loaded, textTracks
|
||||
// will not trigger addtrack
|
||||
for (let i = 0; i < tt.length; i++) {
|
||||
this.textTracks().addTrack_(tt[i]);
|
||||
}
|
||||
|
||||
if (tt.addEventListener) {
|
||||
tt.addEventListener('change', this.handleTextTrackChange_);
|
||||
tt.addEventListener('addtrack', this.handleTextTrackAdd_);
|
||||
tt.addEventListener('removetrack', this.handleTextTrackRemove_);
|
||||
}
|
||||
|
||||
// Remove (native) texttracks that are not used anymore
|
||||
this.on('loadstart', this.removeOldTextTracks_);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle any {@link TextTrackList} `change` event.
|
||||
*
|
||||
* @param {EventTarget~Event} e
|
||||
* The `change` event that caused this to run.
|
||||
*
|
||||
* @listens TextTrackList#change
|
||||
*/
|
||||
handleTextTrackChange(e) {
|
||||
const tt = this.textTracks();
|
||||
|
||||
this.textTracks().trigger({
|
||||
type: 'change',
|
||||
target: tt,
|
||||
currentTarget: tt,
|
||||
srcElement: tt
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle any {@link TextTrackList} `addtrack` event.
|
||||
*
|
||||
* @param {EventTarget~Event} e
|
||||
* The `addtrack` event that caused this to run.
|
||||
*
|
||||
* @listens TextTrackList#addtrack
|
||||
*/
|
||||
handleTextTrackAdd(e) {
|
||||
this.textTracks().addTrack_(e.track);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle any {@link TextTrackList} `removetrack` event.
|
||||
*
|
||||
* @param {EventTarget~Event} e
|
||||
* The `removetrack` event that caused this to run.
|
||||
*
|
||||
* @listens TextTrackList#removetrack
|
||||
*/
|
||||
handleTextTrackRemove(e) {
|
||||
this.textTracks().removeTrack_(e.track);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function removes any {@link AudioTrack}s, {@link VideoTrack}s, or
|
||||
* {@link TextTrack}s that are not in the media elements TrackList.
|
||||
*
|
||||
* @param {TrackList} techTracks
|
||||
* HTML5 Tech's TrackList to search through
|
||||
*
|
||||
* @param {TrackList} elTracks
|
||||
* HTML5 media elements TrackList to search trough.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
removeOldTracks_(techTracks, elTracks) {
|
||||
// This will loop over the techTracks and check if they are still used by the HTML5 media element
|
||||
// If not, they will be removed from the emulated list
|
||||
const removeTracks = [];
|
||||
|
||||
if (!elTracks) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < techTracks.length; i++) {
|
||||
const techTrack = techTracks[i];
|
||||
let found = false;
|
||||
|
||||
for (let j = 0; j < elTracks.length; j++) {
|
||||
if (elTracks[j] === techTrack) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
removeTracks.push(techTrack);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < removeTracks.length; i++) {
|
||||
const track = removeTracks[i];
|
||||
|
||||
techTracks.removeTrack_(track);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove {@link TextTrack}s that dont exist in the native track list from our
|
||||
* emulated {@link TextTrackList}.
|
||||
*
|
||||
* @listens Tech#loadstart
|
||||
*/
|
||||
removeOldTextTracks_(e) {
|
||||
const techTracks = this.textTracks();
|
||||
const elTracks = this.el().textTracks;
|
||||
|
||||
this.removeOldTracks_(techTracks, elTracks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by {@link Player#play} to play using the `Html5` `Tech`.
|
||||
*/
|
||||
@@ -834,6 +719,29 @@ Html5.isSupported = function() {
|
||||
return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the tech can support the given type
|
||||
*
|
||||
* @param {string} type
|
||||
* The mimetype to check
|
||||
* @return {string} 'probably', 'maybe', or '' (empty string)
|
||||
*/
|
||||
Html5.canPlayType = function(type) {
|
||||
return Html5.TEST_VID.canPlayType(type);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the tech can support the given source
|
||||
* @param {Object} srcObj
|
||||
* The source object
|
||||
* @param {Object} options
|
||||
* The options passed to the tech
|
||||
* @return {string} 'probably', 'maybe', or '' (empty string)
|
||||
*/
|
||||
Html5.canPlaySource = function(srcObj, options) {
|
||||
return Html5.canPlayType(srcObj.type);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the volume can be changed in this browser/device.
|
||||
* Volume cannot be changed in a lot of mobile devices.
|
||||
@@ -1188,6 +1096,21 @@ Html5.resetMediaElement = function(el) {
|
||||
*/
|
||||
'muted',
|
||||
|
||||
/**
|
||||
* Get the value of `defaultMuted` from the media element. `defaultMuted` indicates
|
||||
* that the volume for the media should be set to silent when the video first starts.
|
||||
* This does not actually change the `volume` attribute. After playback has started `muted`
|
||||
* will indicate the current status of the volume and `defaultMuted` will not.
|
||||
*
|
||||
* @method Html5.prototype.defaultMuted
|
||||
* @return {boolean}
|
||||
* - True if the value of `volume` should be ignored and the audio set to silent.
|
||||
* - False if the value of `volume` should be used.
|
||||
*
|
||||
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
|
||||
*/
|
||||
'defaultMuted',
|
||||
|
||||
/**
|
||||
* Get the value of `poster` from the media element. `poster` indicates
|
||||
* that the url of an image file that can/will be shown when no media data is available.
|
||||
@@ -1323,7 +1246,7 @@ Html5.resetMediaElement = function(el) {
|
||||
/**
|
||||
* Get the value of `defaultMuted` from the media element. `defaultMuted` indicates
|
||||
* whether the media should start muted or not. Only changes the default state of the
|
||||
* media. `muted` and `defaultMuted` can have different values. `muted` indicates the
|
||||
* media. `muted` and `defaultMuted` can have different values. {@link Html5#muted} indicates the
|
||||
* current state.
|
||||
*
|
||||
* @method Html5#defaultMuted
|
||||
@@ -1351,6 +1274,24 @@ Html5.resetMediaElement = function(el) {
|
||||
*/
|
||||
'playbackRate',
|
||||
|
||||
/**
|
||||
* Get the value of `defaultPlaybackRate` from the media element. `defaultPlaybackRate` indicates
|
||||
* the rate at which the media is currently playing back. This value will not indicate the current
|
||||
* `playbackRate` after playback has started, use {@link Html5#playbackRate} for that.
|
||||
*
|
||||
* Examples:
|
||||
* - if defaultPlaybackRate is set to 2, media will play twice as fast.
|
||||
* - if defaultPlaybackRate is set to 0.5, media will play half as fast.
|
||||
*
|
||||
* @method Html5.prototype.defaultPlaybackRate
|
||||
* @return {number}
|
||||
* The value of `defaultPlaybackRate` from the media element. A number indicating
|
||||
* the current playback speed of the media, where 1 is normal speed.
|
||||
*
|
||||
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
|
||||
*/
|
||||
'defaultPlaybackRate',
|
||||
|
||||
/**
|
||||
* Get the value of `played` from the media element. `played` returns a `TimeRange`
|
||||
* object representing points in the media timeline that have been played.
|
||||
@@ -1448,7 +1389,7 @@ Html5.resetMediaElement = function(el) {
|
||||
'volume',
|
||||
|
||||
/**
|
||||
* Set the value of `muted` on the media element. `muted` indicates the current
|
||||
* Set the value of `muted` on the media element. `muted` indicates that the current
|
||||
* audio level should be silent.
|
||||
*
|
||||
* @method Html5#setMuted
|
||||
@@ -1460,6 +1401,19 @@ Html5.resetMediaElement = function(el) {
|
||||
*/
|
||||
'muted',
|
||||
|
||||
/**
|
||||
* Set the value of `defaultMuted` on the media element. `defaultMuted` indicates that the current
|
||||
* audio level should be silent, but will only effect the muted level on intial playback..
|
||||
*
|
||||
* @method Html5.prototype.setDefaultMuted
|
||||
* @param {boolean} defaultMuted
|
||||
* - True if the audio should be set to silent
|
||||
* - False otherwise
|
||||
*
|
||||
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
|
||||
*/
|
||||
'defaultMuted',
|
||||
|
||||
/**
|
||||
* Set the value of `src` on the media element. `src` indicates the current
|
||||
* {@link Tech~SourceObject} for the media.
|
||||
@@ -1546,7 +1500,26 @@ Html5.resetMediaElement = function(el) {
|
||||
*
|
||||
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
|
||||
*/
|
||||
'playbackRate'
|
||||
'playbackRate',
|
||||
|
||||
/**
|
||||
* Set the value of `defaultPlaybackRate` on the media element. `defaultPlaybackRate` indicates
|
||||
* the rate at which the media should play back upon initial startup. Changing this value
|
||||
* after a video has started will do nothing. Instead you should used {@link Html5#setPlaybackRate}.
|
||||
*
|
||||
* Example Values:
|
||||
* - if playbackRate is set to 2, media will play twice as fast.
|
||||
* - if playbackRate is set to 0.5, media will play half as fast.
|
||||
*
|
||||
* @method Html5.prototype.setDefaultPlaybackRate
|
||||
* @return {number}
|
||||
* The value of `defaultPlaybackRate` from the media element. A number indicating
|
||||
* the current playback speed of the media, where 1 is normal speed.
|
||||
*
|
||||
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultplaybackrate}
|
||||
*/
|
||||
'defaultPlaybackRate'
|
||||
|
||||
].forEach(function(prop) {
|
||||
Html5.prototype['set' + toTitleCase(prop)] = function(v) {
|
||||
this.el_[prop] = v;
|
||||
@@ -1571,7 +1544,16 @@ Html5.resetMediaElement = function(el) {
|
||||
* @method Html5#load
|
||||
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-load}
|
||||
*/
|
||||
'load'
|
||||
'load',
|
||||
|
||||
/**
|
||||
* A wrapper around the media elements `play` function. This will call the `HTML5`s
|
||||
* media element `play` function.
|
||||
*
|
||||
* @method Html5#play
|
||||
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-play}
|
||||
*/
|
||||
'play'
|
||||
].forEach(function(prop) {
|
||||
Html5.prototype[prop] = function() {
|
||||
return this.el_[prop]();
|
||||
@@ -1662,6 +1644,5 @@ Html5.nativeSourceHandler.dispose = function() {};
|
||||
// Register the native source handler
|
||||
Html5.registerSourceHandler(Html5.nativeSourceHandler);
|
||||
|
||||
Component.registerComponent('Html5', Html5);
|
||||
Tech.registerTech('Html5', Html5);
|
||||
export default Html5;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { assign } from '../utils/obj.js';
|
||||
|
||||
const middlewares = {};
|
||||
|
||||
export function use(type, middleware) {
|
||||
middlewares[type] = middlewares[type] || [];
|
||||
middlewares[type].push(middleware);
|
||||
}
|
||||
|
||||
export function getMiddleware(type) {
|
||||
if (type) {
|
||||
return middlewares[type];
|
||||
}
|
||||
|
||||
return middlewares;
|
||||
}
|
||||
|
||||
export function setSource(player, src, next) {
|
||||
player.setTimeout(() => setSourceHelper(src, middlewares[src.type], next, player), 1);
|
||||
}
|
||||
|
||||
export function setTech(middleware, tech) {
|
||||
middleware.forEach((mw) => mw.setTech && mw.setTech(tech));
|
||||
}
|
||||
|
||||
export function get(middleware, tech, method) {
|
||||
return middleware.reduceRight(middlewareIterator(method), tech[method]());
|
||||
}
|
||||
|
||||
export function set(middleware, tech, method, arg) {
|
||||
return tech[method](middleware.reduce(middlewareIterator(method), arg));
|
||||
}
|
||||
|
||||
export const allowedGetters = {
|
||||
buffered: 1,
|
||||
currentTime: 1,
|
||||
duration: 1,
|
||||
seekable: 1,
|
||||
played: 1
|
||||
};
|
||||
|
||||
export const allowedSetters = {
|
||||
setCurrentTime: 1
|
||||
};
|
||||
|
||||
function middlewareIterator(method) {
|
||||
return (value, mw) => {
|
||||
if (mw[method]) {
|
||||
return mw[method](value);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
function setSourceHelper(src = {}, middleware = [], next, player, acc = [], lastRun = false) {
|
||||
const [mwFactory, ...mwrest] = middleware;
|
||||
|
||||
// if mwFactory is a string, then we're at a fork in the road
|
||||
if (typeof mwFactory === 'string') {
|
||||
setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun);
|
||||
|
||||
// if we have an mwFactory, call it with the player to get the mw,
|
||||
// then call the mw's setSource method
|
||||
} else if (mwFactory) {
|
||||
const mw = mwFactory(player);
|
||||
|
||||
mw.setSource(assign({}, src), function(err, _src) {
|
||||
|
||||
// something happened, try the next middleware on the current level
|
||||
// make sure to use the old src
|
||||
if (err) {
|
||||
return setSourceHelper(src, mwrest, next, player, acc, lastRun);
|
||||
}
|
||||
|
||||
// we've succeeded, now we need to go deeper
|
||||
acc.push(mw);
|
||||
|
||||
// if it's the same time, continue does the current chain
|
||||
// otherwise, we want to go down the new chain
|
||||
setSourceHelper(_src,
|
||||
src.type === _src.type ? mwrest : middlewares[_src.type],
|
||||
next,
|
||||
player,
|
||||
acc,
|
||||
lastRun);
|
||||
});
|
||||
} else if (mwrest.length) {
|
||||
setSourceHelper(src, mwrest, next, player, acc, lastRun);
|
||||
} else if (lastRun) {
|
||||
next(src, acc);
|
||||
} else {
|
||||
setSourceHelper(src, middlewares['*'], next, player, acc, true);
|
||||
}
|
||||
}
|
||||
+141
-166
@@ -3,13 +3,7 @@
|
||||
*/
|
||||
|
||||
import Component from '../component';
|
||||
import HTMLTrackElement from '../tracks/html-track-element';
|
||||
import HTMLTrackElementList from '../tracks/html-track-element-list';
|
||||
import mergeOptions from '../utils/merge-options.js';
|
||||
import TextTrack from '../tracks/text-track';
|
||||
import TextTrackList from '../tracks/text-track-list';
|
||||
import VideoTrackList from '../tracks/video-track-list';
|
||||
import AudioTrackList from '../tracks/audio-track-list';
|
||||
import * as Fn from '../utils/fn.js';
|
||||
import log from '../utils/log.js';
|
||||
import { createTimeRange } from '../utils/time-ranges.js';
|
||||
@@ -18,6 +12,7 @@ import MediaError from '../media-error.js';
|
||||
import window from 'global/window';
|
||||
import document from 'global/document';
|
||||
import {isPlain} from '../utils/obj';
|
||||
import * as TRACK_TYPES from '../tracks/track-types';
|
||||
|
||||
/**
|
||||
* An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string
|
||||
@@ -68,9 +63,9 @@ function createTrackHelper(self, kind, label, language, options = {}) {
|
||||
}
|
||||
options.tech = self;
|
||||
|
||||
const track = new TextTrack(options);
|
||||
const track = new TRACK_TYPES.ALL.text.TrackClass(options);
|
||||
|
||||
tracks.addTrack_(track);
|
||||
tracks.addTrack(track);
|
||||
|
||||
return track;
|
||||
}
|
||||
@@ -108,9 +103,13 @@ class Tech extends Component {
|
||||
this.hasStarted_ = false;
|
||||
});
|
||||
|
||||
this.textTracks_ = options.textTracks;
|
||||
this.videoTracks_ = options.videoTracks;
|
||||
this.audioTracks_ = options.audioTracks;
|
||||
TRACK_TYPES.ALL.names.forEach((name) => {
|
||||
const props = TRACK_TYPES.ALL[name];
|
||||
|
||||
if (options && options[props.getterName]) {
|
||||
this[props.privateName] = options[props.getterName];
|
||||
}
|
||||
});
|
||||
|
||||
// Manually track progress in cases where the browser/flash player doesn't report it.
|
||||
if (!this.featuresProgressEvents) {
|
||||
@@ -136,9 +135,8 @@ class Tech extends Component {
|
||||
this.emulateTextTracks();
|
||||
}
|
||||
|
||||
this.autoRemoteTextTracks_ = new TextTrackList();
|
||||
this.autoRemoteTextTracks_ = new TRACK_TYPES.ALL.text.ListClass();
|
||||
|
||||
this.initTextTrackListeners();
|
||||
this.initTrackListeners();
|
||||
|
||||
// Turn on component tap events only if not using native controls
|
||||
@@ -332,7 +330,7 @@ class Tech extends Component {
|
||||
dispose() {
|
||||
|
||||
// clear out all tracks because we can't reuse them between techs
|
||||
this.clearTracks(['audio', 'video', 'text']);
|
||||
this.clearTracks(TRACK_TYPES.NORMAL.names);
|
||||
|
||||
// Turn off any manual progress or timeupdate tracking
|
||||
if (this.manualProgress) {
|
||||
@@ -369,7 +367,7 @@ class Tech extends Component {
|
||||
if (type === 'text') {
|
||||
this.removeRemoteTextTrack(track);
|
||||
}
|
||||
list.removeTrack_(track);
|
||||
list.removeTrack(track);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -450,49 +448,16 @@ class Tech extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn on listeners for {@link TextTrackList} events. This adds
|
||||
* {@link EventTarget~EventListeners} for `texttrackchange`, `addtrack` and
|
||||
* `removetrack`.
|
||||
* Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and
|
||||
* {@link TextTrackList} events.
|
||||
*
|
||||
* @fires Tech#texttrackchange
|
||||
*/
|
||||
initTextTrackListeners() {
|
||||
const textTrackListChanges = Fn.bind(this, function() {
|
||||
/**
|
||||
* Triggered when tracks are added or removed on the Tech {@link TextTrackList}
|
||||
*
|
||||
* @event Tech#texttrackchange
|
||||
* @type {EventTarget~Event}
|
||||
*/
|
||||
this.trigger('texttrackchange');
|
||||
});
|
||||
|
||||
const tracks = this.textTracks();
|
||||
|
||||
if (!tracks) {
|
||||
return;
|
||||
}
|
||||
|
||||
tracks.addEventListener('removetrack', textTrackListChanges);
|
||||
tracks.addEventListener('addtrack', textTrackListChanges);
|
||||
|
||||
this.on('dispose', Fn.bind(this, function() {
|
||||
tracks.removeEventListener('removetrack', textTrackListChanges);
|
||||
tracks.removeEventListener('addtrack', textTrackListChanges);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn on listeners for {@link VideoTrackList} and {@link {AudioTrackList} events.
|
||||
* This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`.
|
||||
*
|
||||
* @fires Tech#audiotrackchange
|
||||
* @fires Tech#videotrackchange
|
||||
* @fires Tech#texttrackchange
|
||||
*/
|
||||
initTrackListeners() {
|
||||
const trackTypes = ['video', 'audio'];
|
||||
|
||||
trackTypes.forEach((type) => {
|
||||
/**
|
||||
* Triggered when tracks are added or removed on the Tech {@link AudioTrackList}
|
||||
*
|
||||
@@ -506,11 +471,20 @@ class Tech extends Component {
|
||||
* @event Tech#videotrackchange
|
||||
* @type {EventTarget~Event}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Triggered when tracks are added or removed on the Tech {@link TextTrackList}
|
||||
*
|
||||
* @event Tech#texttrackchange
|
||||
* @type {EventTarget~Event}
|
||||
*/
|
||||
TRACK_TYPES.NORMAL.names.forEach((name) => {
|
||||
const props = TRACK_TYPES.NORMAL[name];
|
||||
const trackListChanges = () => {
|
||||
this.trigger(`${type}trackchange`);
|
||||
this.trigger(`${name}trackchange`);
|
||||
};
|
||||
|
||||
const tracks = this[`${type}Tracks`]();
|
||||
const tracks = this[props.getterName]();
|
||||
|
||||
tracks.addEventListener('removetrack', trackListChanges);
|
||||
tracks.addEventListener('addtrack', trackListChanges);
|
||||
@@ -584,18 +558,12 @@ class Tech extends Component {
|
||||
*/
|
||||
emulateTextTracks() {
|
||||
const tracks = this.textTracks();
|
||||
const remoteTracks = this.remoteTextTracks();
|
||||
const handleAddTrack = (e) => tracks.addTrack(e.track);
|
||||
const handleRemoveTrack = (e) => tracks.removeTrack(e.track);
|
||||
|
||||
if (!tracks) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.remoteTextTracks().on('addtrack', (e) => {
|
||||
this.textTracks().addTrack_(e.track);
|
||||
});
|
||||
|
||||
this.remoteTextTracks().on('removetrack', (e) => {
|
||||
this.textTracks().removeTrack_(e.track);
|
||||
});
|
||||
remoteTracks.on('addtrack', handleAddTrack);
|
||||
remoteTracks.on('removetrack', handleRemoveTrack);
|
||||
|
||||
// Initially, Tech.el_ is a child of a dummy-div wait until the Component system
|
||||
// signals that the Tech is ready at which point Tech.el_ is part of the DOM
|
||||
@@ -603,6 +571,7 @@ class Tech extends Component {
|
||||
this.on('ready', this.addWebVttScript_);
|
||||
|
||||
const updateDisplay = () => this.trigger('texttrackchange');
|
||||
|
||||
const textTracksChanges = () => {
|
||||
updateDisplay();
|
||||
|
||||
@@ -620,67 +589,18 @@ class Tech extends Component {
|
||||
tracks.addEventListener('change', textTracksChanges);
|
||||
|
||||
this.on('dispose', function() {
|
||||
remoteTracks.off('addtrack', handleAddTrack);
|
||||
remoteTracks.off('removetrack', handleRemoveTrack);
|
||||
tracks.removeEventListener('change', textTracksChanges);
|
||||
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
const track = tracks[i];
|
||||
|
||||
track.removeEventListener('cuechange', updateDisplay);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `Tech`s {@link VideoTrackList}.
|
||||
*
|
||||
* @return {VideoTrackList}
|
||||
* The video track list that the Tech is currently using.
|
||||
*/
|
||||
videoTracks() {
|
||||
this.videoTracks_ = this.videoTracks_ || new VideoTrackList();
|
||||
return this.videoTracks_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `Tech`s {@link AudioTrackList}.
|
||||
*
|
||||
* @return {AudioTrackList}
|
||||
* The audio track list that the Tech is currently using.
|
||||
*/
|
||||
audioTracks() {
|
||||
this.audioTracks_ = this.audioTracks_ || new AudioTrackList();
|
||||
return this.audioTracks_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `Tech`s {@link TextTrackList}.
|
||||
*
|
||||
* @return {TextTrackList}
|
||||
* The text track list that the Tech is currently using.
|
||||
*/
|
||||
textTracks() {
|
||||
this.textTracks_ = this.textTracks_ || new TextTrackList();
|
||||
return this.textTracks_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `Tech`s remote {@link TextTrackList}, which is created from elements
|
||||
* that were added to the DOM.
|
||||
*
|
||||
* @return {TextTrackList}
|
||||
* The remote text track list that the Tech is currently using.
|
||||
*/
|
||||
remoteTextTracks() {
|
||||
this.remoteTextTracks_ = this.remoteTextTracks_ || new TextTrackList();
|
||||
return this.remoteTextTracks_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get The `Tech`s {HTMLTrackElementList}, which are the elements in the DOM that are
|
||||
* being used as TextTracks.
|
||||
*
|
||||
* @return {HTMLTrackElementList}
|
||||
* The current HTML track elements that exist for the tech.
|
||||
*/
|
||||
remoteTextTrackEls() {
|
||||
this.remoteTextTrackEls_ = this.remoteTextTrackEls_ || new HTMLTrackElementList();
|
||||
return this.remoteTextTrackEls_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and returns a remote {@link TextTrack} object.
|
||||
*
|
||||
@@ -730,7 +650,7 @@ class Tech extends Component {
|
||||
tech: this
|
||||
});
|
||||
|
||||
return new HTMLTrackElement(track);
|
||||
return new TRACK_TYPES.REMOTE.remoteTextEl.TrackClass(track);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -764,11 +684,11 @@ class Tech extends Component {
|
||||
|
||||
// store HTMLTrackElement and TextTrack to remote list
|
||||
this.remoteTextTrackEls().addTrackElement_(htmlTrackElement);
|
||||
this.remoteTextTracks().addTrack_(htmlTrackElement.track);
|
||||
this.remoteTextTracks().addTrack(htmlTrackElement.track);
|
||||
|
||||
if (manualCleanup !== true) {
|
||||
// create the TextTrackList if it doesn't exist
|
||||
this.autoRemoteTextTracks_.addTrack_(htmlTrackElement.track);
|
||||
this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track);
|
||||
}
|
||||
|
||||
return htmlTrackElement;
|
||||
@@ -785,8 +705,8 @@ class Tech extends Component {
|
||||
|
||||
// remove HTMLTrackElement and TextTrack from remote list
|
||||
this.remoteTextTrackEls().removeTrackElement_(trackElement);
|
||||
this.remoteTextTracks().removeTrack_(track);
|
||||
this.autoRemoteTextTracks_.removeTrack_(track);
|
||||
this.remoteTextTracks().removeTrack(track);
|
||||
this.autoRemoteTextTracks_.removeTrack(track);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -816,6 +736,32 @@ class Tech extends Component {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the type is supported by this tech.
|
||||
*
|
||||
* The base tech does not support any type, but source handlers might
|
||||
* overwrite this.
|
||||
*
|
||||
* @param {string} type
|
||||
* The media type to check
|
||||
* @return {string} Returns the native video element's response
|
||||
*/
|
||||
static canPlayType() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the tech can support the given source
|
||||
* @param {Object} srcObj
|
||||
* The source object
|
||||
* @param {Object} options
|
||||
* The options passed to the tech
|
||||
* @return {string} 'probably', 'maybe', or '' (empty string)
|
||||
*/
|
||||
static canPlaySource(srcObj, options) {
|
||||
return Tech.canPlayType(srcObj.type);
|
||||
}
|
||||
|
||||
/*
|
||||
* Return whether the argument is a Tech or not.
|
||||
* Can be passed either a Class like `Html5` or a instance like `player.tech_`
|
||||
@@ -852,7 +798,18 @@ class Tech extends Component {
|
||||
throw new Error(`Tech ${name} must be a Tech`);
|
||||
}
|
||||
|
||||
if (!Tech.canPlayType) {
|
||||
throw new Error('Techs must have a static canPlayType method on them');
|
||||
}
|
||||
if (!Tech.canPlaySource) {
|
||||
throw new Error('Techs must have a static canPlaySource method on them');
|
||||
}
|
||||
|
||||
Tech.techs_[name] = tech;
|
||||
if (name !== 'Tech') {
|
||||
// camel case the techName for use in techOrder
|
||||
Tech.defaultTechs_.push(name.charAt(0).toLowerCase() + name.slice(1));
|
||||
}
|
||||
return tech;
|
||||
}
|
||||
|
||||
@@ -878,28 +835,72 @@ class Tech extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* List of associated text tracks.
|
||||
* Get the {@link VideoTrackList}
|
||||
*
|
||||
* @returns {VideoTrackList}
|
||||
* @method Tech.prototype.videoTracks
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the {@link AudioTrackList}
|
||||
*
|
||||
* @returns {AudioTrackList}
|
||||
* @method Tech.prototype.audioTracks
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the {@link TextTrackList}
|
||||
*
|
||||
* @returns {TextTrackList}
|
||||
* @method Tech.prototype.textTracks
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the remote element {@link TextTrackList}
|
||||
*
|
||||
* @returns {TextTrackList}
|
||||
* @method Tech.prototype.remoteTextTracks
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the remote element {@link HTMLTrackElementList}
|
||||
*
|
||||
* @returns {HTMLTrackElementList}
|
||||
* @method Tech.prototype.remoteTextTrackEls
|
||||
*/
|
||||
|
||||
TRACK_TYPES.ALL.names.forEach(function(name) {
|
||||
const props = TRACK_TYPES.ALL[name];
|
||||
|
||||
Tech.prototype[props.getterName] = function() {
|
||||
this[props.privateName] = this[props.privateName] || new props.ListClass();
|
||||
return this[props.privateName];
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* List of associated text tracks
|
||||
*
|
||||
* @type {TextTrackList}
|
||||
* @private
|
||||
* @property Tech#textTracks_
|
||||
*/
|
||||
Tech.prototype.textTracks_; // eslint-disable-line
|
||||
|
||||
/**
|
||||
* List of associated audio tracks.
|
||||
*
|
||||
* @type {AudioTrackList}
|
||||
* @private
|
||||
* @property Tech#audioTracks_
|
||||
*/
|
||||
Tech.prototype.audioTracks_; // eslint-disable-line
|
||||
|
||||
/**
|
||||
* List of associated video tracks.
|
||||
*
|
||||
* @type {VideoTrackList}
|
||||
* @private
|
||||
* @property Tech#videoTracks_
|
||||
*/
|
||||
Tech.prototype.videoTracks_; // eslint-disable-line
|
||||
|
||||
/**
|
||||
* Boolean indicating wether the `Tech` supports volume control.
|
||||
@@ -1118,9 +1119,6 @@ Tech.withSourceHandlers = function(_Tech) {
|
||||
*
|
||||
* @param {Tech~SourceObject} source
|
||||
* A source object with src and type keys
|
||||
*
|
||||
* @return {Tech}
|
||||
* Returns itself; this method is chainable
|
||||
*/
|
||||
_Tech.prototype.setSource = function(source) {
|
||||
let sh = _Tech.selectSourceHandler(source, this.options_);
|
||||
@@ -1141,38 +1139,10 @@ Tech.withSourceHandlers = function(_Tech) {
|
||||
|
||||
if (sh !== _Tech.nativeSourceHandler) {
|
||||
this.currentSource_ = source;
|
||||
|
||||
// Catch if someone replaced the src without calling setSource.
|
||||
// If they do, set currentSource_ to null and dispose our source handler.
|
||||
this.off(this.el_, 'loadstart', _Tech.prototype.firstLoadStartListener_);
|
||||
this.off(this.el_, 'loadstart', _Tech.prototype.successiveLoadStartListener_);
|
||||
this.one(this.el_, 'loadstart', _Tech.prototype.firstLoadStartListener_);
|
||||
}
|
||||
|
||||
this.sourceHandler_ = sh.handleSource(source, this, this.options_);
|
||||
this.on('dispose', this.disposeSourceHandler);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Called once for the first loadstart of a video.
|
||||
*
|
||||
* @listens Tech#loadstart
|
||||
*/
|
||||
_Tech.prototype.firstLoadStartListener_ = function() {
|
||||
this.one(this.el_, 'loadstart', _Tech.prototype.successiveLoadStartListener_);
|
||||
};
|
||||
|
||||
// On successive loadstarts when setSource has not been called again
|
||||
/**
|
||||
* Called after the first loadstart for a video occurs.
|
||||
*
|
||||
* @listens Tech#loadstart
|
||||
*/
|
||||
_Tech.prototype.successiveLoadStartListener_ = function() {
|
||||
this.disposeSourceHandler();
|
||||
this.one(this.el_, 'loadstart', _Tech.prototype.successiveLoadStartListener_);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1193,8 +1163,6 @@ Tech.withSourceHandlers = function(_Tech) {
|
||||
this.cleanupAutoTextTracks();
|
||||
|
||||
if (this.sourceHandler_) {
|
||||
this.off(this.el_, 'loadstart', _Tech.prototype.firstLoadStartListener_);
|
||||
this.off(this.el_, 'loadstart', _Tech.prototype.successiveLoadStartListener_);
|
||||
|
||||
if (this.sourceHandler_.dispose) {
|
||||
this.sourceHandler_.dispose();
|
||||
@@ -1206,9 +1174,16 @@ Tech.withSourceHandlers = function(_Tech) {
|
||||
|
||||
};
|
||||
|
||||
// The base Tech class needs to be registered as a Component. It is the only
|
||||
// Tech that can be registered as a Component.
|
||||
Component.registerComponent('Tech', Tech);
|
||||
// Old name for Tech
|
||||
// @deprecated
|
||||
Component.registerComponent('MediaTechController', Tech);
|
||||
Tech.registerTech('Tech', Tech);
|
||||
|
||||
/**
|
||||
* A list of techs that should be added to techOrder on Players
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Tech.defaultTechs_ = [];
|
||||
|
||||
export default Tech;
|
||||
|
||||
@@ -81,15 +81,14 @@ class AudioTrackList extends TrackList {
|
||||
* @param {AudioTrack} track
|
||||
* The AudioTrack to add to the list
|
||||
*
|
||||
* @fires Track#addtrack
|
||||
* @private
|
||||
* @fires TrackList#addtrack
|
||||
*/
|
||||
addTrack_(track) {
|
||||
addTrack(track) {
|
||||
if (track.enabled) {
|
||||
disableOthers(this, track);
|
||||
}
|
||||
|
||||
super.addTrack_(track);
|
||||
super.addTrack(track);
|
||||
// native tracks don't have this
|
||||
if (!track.addEventListener) {
|
||||
return;
|
||||
@@ -112,31 +111,6 @@ class AudioTrackList extends TrackList {
|
||||
this.trigger('change');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an {@link AudioTrack} to the `AudioTrackList`.
|
||||
*
|
||||
* @param {AudioTrack} track
|
||||
* The AudioTrack to add to the list
|
||||
*
|
||||
* @fires Track#addtrack
|
||||
*/
|
||||
addTrack(track) {
|
||||
this.addTrack_(track);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an {@link AudioTrack} from the `AudioTrackList`.
|
||||
*
|
||||
* @param {AudioTrack} track
|
||||
* The AudioTrack to remove from the list
|
||||
*
|
||||
* @fires Track#removetrack
|
||||
*/
|
||||
removeTrack(track) {
|
||||
super.removeTrack_(track);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AudioTrackList;
|
||||
|
||||
@@ -114,28 +114,26 @@ class TextTrackDisplay extends Component {
|
||||
let firstDesc;
|
||||
let firstCaptions;
|
||||
|
||||
if (trackList) {
|
||||
for (let i = 0; i < trackList.length; i++) {
|
||||
const track = trackList[i];
|
||||
for (let i = 0; i < trackList.length; i++) {
|
||||
const track = trackList[i];
|
||||
|
||||
if (track.default) {
|
||||
if (track.kind === 'descriptions' && !firstDesc) {
|
||||
firstDesc = track;
|
||||
} else if (track.kind in modes && !firstCaptions) {
|
||||
firstCaptions = track;
|
||||
}
|
||||
if (track.default) {
|
||||
if (track.kind === 'descriptions' && !firstDesc) {
|
||||
firstDesc = track;
|
||||
} else if (track.kind in modes && !firstCaptions) {
|
||||
firstCaptions = track;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We want to show the first default track but captions and subtitles
|
||||
// take precedence over descriptions.
|
||||
// So, display the first default captions or subtitles track
|
||||
// and otherwise the first default descriptions track.
|
||||
if (firstCaptions) {
|
||||
firstCaptions.mode = 'showing';
|
||||
} else if (firstDesc) {
|
||||
firstDesc.mode = 'showing';
|
||||
}
|
||||
// We want to show the first default track but captions and subtitles
|
||||
// take precedence over descriptions.
|
||||
// So, display the first default captions or subtitles track
|
||||
// and otherwise the first default descriptions track.
|
||||
if (firstCaptions) {
|
||||
firstCaptions.mode = 'showing';
|
||||
} else if (firstDesc) {
|
||||
firstDesc.mode = 'showing';
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -192,17 +190,12 @@ class TextTrackDisplay extends Component {
|
||||
|
||||
this.clearDisplay();
|
||||
|
||||
if (!tracks) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Track display prioritization model: if multiple tracks are 'showing',
|
||||
// display the first 'subtitles' or 'captions' track which is 'showing',
|
||||
// otherwise display the first 'descriptions' track which is 'showing'
|
||||
|
||||
let descriptionsTrack = null;
|
||||
let captionsSubtitlesTrack = null;
|
||||
|
||||
let i = tracks.length;
|
||||
|
||||
while (i--) {
|
||||
|
||||
@@ -50,10 +50,9 @@ class TextTrackList extends TrackList {
|
||||
* The text track to add to the list.
|
||||
*
|
||||
* @fires TrackList#addtrack
|
||||
* @private
|
||||
*/
|
||||
addTrack_(track) {
|
||||
super.addTrack_(track);
|
||||
addTrack(track) {
|
||||
super.addTrack(track);
|
||||
|
||||
/**
|
||||
* @listens TextTrack#modechange
|
||||
|
||||
@@ -297,7 +297,7 @@ class TextTrackSettings extends Component {
|
||||
return [
|
||||
createEl('label', {
|
||||
className: 'vjs-label',
|
||||
textContent: config.label
|
||||
textContent: this.localize(config.label)
|
||||
}, {
|
||||
for: id
|
||||
}),
|
||||
@@ -442,7 +442,7 @@ class TextTrackSettings extends Component {
|
||||
|
||||
const doneButton = createEl('button', {
|
||||
className: 'vjs-done-button',
|
||||
textContent: 'Done'
|
||||
textContent: this.localize('Done')
|
||||
});
|
||||
|
||||
return createEl('div', {
|
||||
@@ -468,7 +468,7 @@ class TextTrackSettings extends Component {
|
||||
const heading = createEl('div', {
|
||||
className: 'vjs-control-text',
|
||||
id: `TTsettingsDialogLabel-${this.id_}`,
|
||||
textContent: 'Caption Settings Dialog'
|
||||
textContent: this.localize('Caption Settings Dialog')
|
||||
}, {
|
||||
'aria-level': '1',
|
||||
'role': 'heading'
|
||||
@@ -477,7 +477,7 @@ class TextTrackSettings extends Component {
|
||||
const description = createEl('div', {
|
||||
className: 'vjs-control-text',
|
||||
id: `TTsettingsDialogDescription-${this.id_}`,
|
||||
textContent: 'Beginning of dialog window. Escape will cancel and close the window.'
|
||||
textContent: this.localize('Beginning of dialog window. Escape will cancel and close the window.')
|
||||
});
|
||||
|
||||
const doc = createEl('div', undefined, {
|
||||
|
||||
@@ -331,11 +331,9 @@ class TextTrack extends Track {
|
||||
addCue(cue) {
|
||||
const tracks = this.tech_.textTracks();
|
||||
|
||||
if (tracks) {
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
if (tracks[i] !== this) {
|
||||
tracks[i].removeCue(cue);
|
||||
}
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
if (tracks[i] !== this) {
|
||||
tracks[i].removeCue(cue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class TrackList extends EventTarget {
|
||||
});
|
||||
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
list.addTrack_(tracks[i]);
|
||||
list.addTrack(tracks[i]);
|
||||
}
|
||||
|
||||
// must return the object, as for ie8 it will not be this
|
||||
@@ -65,9 +65,8 @@ class TrackList extends EventTarget {
|
||||
* The audio, video, or text track to add to the list.
|
||||
*
|
||||
* @fires TrackList#addtrack
|
||||
* @private
|
||||
*/
|
||||
addTrack_(track) {
|
||||
addTrack(track) {
|
||||
const index = this.tracks_.length;
|
||||
|
||||
if (!('' + index in this)) {
|
||||
@@ -99,13 +98,12 @@ class TrackList extends EventTarget {
|
||||
/**
|
||||
* Remove a {@link Track} from the `TrackList`
|
||||
*
|
||||
* @param {Track} track
|
||||
* @param {Track} rtrack
|
||||
* The audio, video, or text track to remove from the list.
|
||||
*
|
||||
* @fires TrackList#removetrack
|
||||
* @private
|
||||
*/
|
||||
removeTrack_(rtrack) {
|
||||
removeTrack(rtrack) {
|
||||
let track;
|
||||
|
||||
for (let i = 0, l = this.length; i < l; i++) {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import AudioTrackList from './audio-track-list';
|
||||
import VideoTrackList from './video-track-list';
|
||||
import TextTrackList from './text-track-list';
|
||||
import HTMLTrackElementList from './html-track-element-list';
|
||||
|
||||
import TextTrack from './text-track';
|
||||
import AudioTrack from './audio-track';
|
||||
import VideoTrack from './video-track';
|
||||
import HTMLTrackElement from './html-track-element';
|
||||
|
||||
import mergeOptions from '../utils/merge-options';
|
||||
|
||||
/**
|
||||
* This file contains all track properties that are used in
|
||||
* player.js, tech.js, html5.js and possibly other techs in the future.
|
||||
*/
|
||||
|
||||
const NORMAL = {
|
||||
audio: {
|
||||
ListClass: AudioTrackList,
|
||||
TrackClass: AudioTrack,
|
||||
capitalName: 'Audio'
|
||||
},
|
||||
video: {
|
||||
ListClass: VideoTrackList,
|
||||
TrackClass: VideoTrack,
|
||||
capitalName: 'Video'
|
||||
},
|
||||
text: {
|
||||
ListClass: TextTrackList,
|
||||
TrackClass: TextTrack,
|
||||
capitalName: 'Text'
|
||||
}
|
||||
};
|
||||
|
||||
Object.keys(NORMAL).forEach(function(type) {
|
||||
NORMAL[type].getterName = `${type}Tracks`;
|
||||
NORMAL[type].privateName = `${type}Tracks_`;
|
||||
});
|
||||
|
||||
const REMOTE = {
|
||||
remoteText: {
|
||||
ListClass: TextTrackList,
|
||||
TrackClass: TextTrack,
|
||||
capitalName: 'RemoteText',
|
||||
getterName: 'remoteTextTracks',
|
||||
privateName: 'remoteTextTracks_'
|
||||
},
|
||||
remoteTextEl: {
|
||||
ListClass: HTMLTrackElementList,
|
||||
TrackClass: HTMLTrackElement,
|
||||
capitalName: 'RemoteTextTrackEls',
|
||||
getterName: 'remoteTextTrackEls',
|
||||
privateName: 'remoteTextTrackEls_'
|
||||
}
|
||||
};
|
||||
|
||||
const ALL = mergeOptions(NORMAL, REMOTE);
|
||||
|
||||
REMOTE.names = Object.keys(REMOTE);
|
||||
NORMAL.names = Object.keys(NORMAL);
|
||||
ALL.names = [].concat(REMOTE.names).concat(NORMAL.names);
|
||||
|
||||
export {
|
||||
NORMAL,
|
||||
REMOTE,
|
||||
ALL
|
||||
};
|
||||
@@ -97,14 +97,13 @@ class VideoTrackList extends TrackList {
|
||||
* The VideoTrack to add to the list
|
||||
*
|
||||
* @fires TrackList#addtrack
|
||||
* @private
|
||||
*/
|
||||
addTrack_(track) {
|
||||
addTrack(track) {
|
||||
if (track.selected) {
|
||||
disableOthers(this, track);
|
||||
}
|
||||
|
||||
super.addTrack_(track);
|
||||
super.addTrack(track);
|
||||
// native tracks don't have this
|
||||
if (!track.addEventListener) {
|
||||
return;
|
||||
@@ -124,31 +123,6 @@ class VideoTrackList extends TrackList {
|
||||
this.trigger('change');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a {@link VideoTrack} to the `VideoTrackList`.
|
||||
*
|
||||
* @param {VideoTrack} track
|
||||
* The VideoTrack to add to the list
|
||||
*
|
||||
* @fires TrackList#addtrack
|
||||
*/
|
||||
addTrack(track) {
|
||||
this.addTrack_(track);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a {@link VideoTrack} to the `VideoTrackList`.
|
||||
*
|
||||
* @param {VideoTrack} track
|
||||
* The VideoTrack to remove from the list.
|
||||
*
|
||||
* @fires TrackList#removetrack
|
||||
*/
|
||||
removeTrack(track) {
|
||||
super.removeTrack_(track);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default VideoTrackList;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @file dom-data.js
|
||||
* @module dom-data
|
||||
*/
|
||||
import * as Guid from './guid.js';
|
||||
|
||||
/**
|
||||
* Element Data Store.
|
||||
*
|
||||
* Allows for binding data to an element without putting it directly on the
|
||||
* element. Ex. Event listeners are stored here.
|
||||
* (also from jsninja.com, slightly modified and updated for closure compiler)
|
||||
*
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
const elData = {};
|
||||
|
||||
/*
|
||||
* Unique attribute name to store an element's guid in
|
||||
*
|
||||
* @type {String}
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const elIdAttr = 'vdata' + (new Date()).getTime();
|
||||
|
||||
/**
|
||||
* Returns the cache object where data for an element is stored
|
||||
*
|
||||
* @param {Element} el
|
||||
* Element to store data for.
|
||||
*
|
||||
* @return {Object}
|
||||
* The cache object for that el that was passed in.
|
||||
*/
|
||||
export function getData(el) {
|
||||
let id = el[elIdAttr];
|
||||
|
||||
if (!id) {
|
||||
id = el[elIdAttr] = Guid.newGUID();
|
||||
}
|
||||
|
||||
if (!elData[id]) {
|
||||
elData[id] = {};
|
||||
}
|
||||
|
||||
return elData[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not an element has cached data
|
||||
*
|
||||
* @param {Element} el
|
||||
* Check if this element has cached data.
|
||||
*
|
||||
* @return {boolean}
|
||||
* - True if the DOM element has cached data.
|
||||
* - False otherwise.
|
||||
*/
|
||||
export function hasData(el) {
|
||||
const id = el[elIdAttr];
|
||||
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!Object.getOwnPropertyNames(elData[id]).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete data for the element from the cache and the guid attr from getElementById
|
||||
*
|
||||
* @param {Element} el
|
||||
* Remove cached data for this element.
|
||||
*/
|
||||
export function removeData(el) {
|
||||
const id = el[elIdAttr];
|
||||
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all stored data
|
||||
delete elData[id];
|
||||
|
||||
// Remove the elIdAttr property from the DOM node
|
||||
try {
|
||||
delete el[elIdAttr];
|
||||
} catch (e) {
|
||||
if (el.removeAttribute) {
|
||||
el.removeAttribute(elIdAttr);
|
||||
} else {
|
||||
// IE doesn't appear to support removeAttribute on the document element
|
||||
el[elIdAttr] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
-124
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
import document from 'global/document';
|
||||
import window from 'global/window';
|
||||
import * as Guid from './guid.js';
|
||||
import log from './log.js';
|
||||
import tsml from 'tsml';
|
||||
import {isObject} from './obj';
|
||||
import computedStyle from './computed-style';
|
||||
|
||||
/**
|
||||
* Detect if a value is a string with any non-whitespace characters.
|
||||
@@ -109,24 +109,6 @@ function createQuerier(method) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand for document.getElementById()
|
||||
* Also allows for CSS (jQuery) ID syntax. But nothing other than IDs.
|
||||
*
|
||||
* @param {string} id
|
||||
* The id of the element to get
|
||||
*
|
||||
* @return {Element|null}
|
||||
* Element with supplied ID or null if there wasn't one.
|
||||
*/
|
||||
export function getEl(id) {
|
||||
if (id.indexOf('#') === 0) {
|
||||
id = id.slice(1);
|
||||
}
|
||||
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an element and applies properties.
|
||||
*
|
||||
@@ -209,9 +191,8 @@ export function textContent(el, text) {
|
||||
*
|
||||
* @param {Element} parent
|
||||
* Element to insert child into
|
||||
*
|
||||
*/
|
||||
export function insertElFirst(child, parent) {
|
||||
export function prependTo(child, parent) {
|
||||
if (parent.firstChild) {
|
||||
parent.insertBefore(child, parent.firstChild);
|
||||
} else {
|
||||
@@ -219,97 +200,6 @@ export function insertElFirst(child, parent) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Element Data Store. Allows for binding data to an element without putting it directly on the element.
|
||||
* Ex. Event listeners are stored here.
|
||||
* (also from jsninja.com, slightly modified and updated for closure compiler)
|
||||
*
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
const elData = {};
|
||||
|
||||
/*
|
||||
* Unique attribute name to store an element's guid in
|
||||
*
|
||||
* @type {string}
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const elIdAttr = 'vdata' + (new Date()).getTime();
|
||||
|
||||
/**
|
||||
* Returns the cache object where data for an element is stored
|
||||
*
|
||||
* @param {Element} el
|
||||
* Element to store data for.
|
||||
*
|
||||
* @return {Object}
|
||||
* The cache object for that el that was passed in.
|
||||
*/
|
||||
export function getElData(el) {
|
||||
let id = el[elIdAttr];
|
||||
|
||||
if (!id) {
|
||||
id = el[elIdAttr] = Guid.newGUID();
|
||||
}
|
||||
|
||||
if (!elData[id]) {
|
||||
elData[id] = {};
|
||||
}
|
||||
|
||||
return elData[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not an element has cached data
|
||||
*
|
||||
* @param {Element} el
|
||||
* Check if this element has cached data.
|
||||
*
|
||||
* @return {boolean}
|
||||
* - True if the DOM element has cached data.
|
||||
* - False otherwise.
|
||||
*/
|
||||
export function hasElData(el) {
|
||||
const id = el[elIdAttr];
|
||||
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!Object.getOwnPropertyNames(elData[id]).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete data for the element from the cache and the guid attr from getElementById
|
||||
*
|
||||
* @param {Element} el
|
||||
* Remove cached data for this element.
|
||||
*/
|
||||
export function removeElData(el) {
|
||||
const id = el[elIdAttr];
|
||||
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all stored data
|
||||
delete elData[id];
|
||||
|
||||
// Remove the elIdAttr property from the DOM node
|
||||
try {
|
||||
delete el[elIdAttr];
|
||||
} catch (e) {
|
||||
if (el.removeAttribute) {
|
||||
el.removeAttribute(elIdAttr);
|
||||
} else {
|
||||
// IE doesn't appear to support removeAttribute on the document element
|
||||
el[elIdAttr] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an element has a CSS class
|
||||
*
|
||||
@@ -326,7 +216,7 @@ export function removeElData(el) {
|
||||
* @throws {Error}
|
||||
* Throws an error if `classToCheck` has white space.
|
||||
*/
|
||||
export function hasElClass(element, classToCheck) {
|
||||
export function hasClass(element, classToCheck) {
|
||||
throwIfWhitespace(classToCheck);
|
||||
if (element.classList) {
|
||||
return element.classList.contains(classToCheck);
|
||||
@@ -346,13 +236,13 @@ export function hasElClass(element, classToCheck) {
|
||||
* @return {Element}
|
||||
* The dom element with the added class name.
|
||||
*/
|
||||
export function addElClass(element, classToAdd) {
|
||||
export function addClass(element, classToAdd) {
|
||||
if (element.classList) {
|
||||
element.classList.add(classToAdd);
|
||||
|
||||
// Don't need to `throwIfWhitespace` here because `hasElClass` will do it
|
||||
// in the case of classList not being supported.
|
||||
} else if (!hasElClass(element, classToAdd)) {
|
||||
} else if (!hasClass(element, classToAdd)) {
|
||||
element.className = (element.className + ' ' + classToAdd).trim();
|
||||
}
|
||||
|
||||
@@ -371,7 +261,7 @@ export function addElClass(element, classToAdd) {
|
||||
* @return {Element}
|
||||
* The dom element with class name removed.
|
||||
*/
|
||||
export function removeElClass(element, classToRemove) {
|
||||
export function removeClass(element, classToRemove) {
|
||||
if (element.classList) {
|
||||
element.classList.remove(classToRemove);
|
||||
} else {
|
||||
@@ -416,12 +306,12 @@ export function removeElClass(element, classToRemove) {
|
||||
* @return {Element}
|
||||
* The element with a class that has been toggled.
|
||||
*/
|
||||
export function toggleElClass(element, classToToggle, predicate) {
|
||||
export function toggleClass(element, classToToggle, predicate) {
|
||||
|
||||
// This CANNOT use `classList` internally because IE does not support the
|
||||
// second parameter to the `classList.toggle()` method! Which is fine because
|
||||
// `classList` will be used by the add/remove functions.
|
||||
const has = hasElClass(element, classToToggle);
|
||||
const has = hasClass(element, classToToggle);
|
||||
|
||||
if (typeof predicate === 'function') {
|
||||
predicate = predicate(element, classToToggle);
|
||||
@@ -438,9 +328,9 @@ export function toggleElClass(element, classToToggle, predicate) {
|
||||
}
|
||||
|
||||
if (predicate) {
|
||||
addElClass(element, classToToggle);
|
||||
addClass(element, classToToggle);
|
||||
} else {
|
||||
removeElClass(element, classToToggle);
|
||||
removeClass(element, classToToggle);
|
||||
}
|
||||
|
||||
return element;
|
||||
@@ -455,7 +345,7 @@ export function toggleElClass(element, classToToggle, predicate) {
|
||||
* @param {Object} [attributes]
|
||||
* Attributes to be applied.
|
||||
*/
|
||||
export function setElAttributes(el, attributes) {
|
||||
export function setAttributes(el, attributes) {
|
||||
Object.getOwnPropertyNames(attributes).forEach(function(attrName) {
|
||||
const attrValue = attributes[attrName];
|
||||
|
||||
@@ -479,7 +369,7 @@ export function setElAttributes(el, attributes) {
|
||||
* @return {Object}
|
||||
* All attributes of the element.
|
||||
*/
|
||||
export function getElAttributes(tag) {
|
||||
export function getAttributes(tag) {
|
||||
const obj = {};
|
||||
|
||||
// known boolean attributes
|
||||
@@ -574,6 +464,48 @@ export function unblockTextSelection() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Identical to the native `getBoundingClientRect` function, but ensures that
|
||||
* the method is supported at all (it is in all browsers we claim to support)
|
||||
* and that the element is in the DOM before continuing.
|
||||
*
|
||||
* This wrapper function also shims properties which are not provided by some
|
||||
* older browsers (namely, IE8).
|
||||
*
|
||||
* Additionally, some browsers do not support adding properties to a
|
||||
* `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard
|
||||
* properties (except `x` and `y` which are not widely supported). This helps
|
||||
* avoid implementations where keys are non-enumerable.
|
||||
*
|
||||
* @param {Element} el
|
||||
* Element whose `ClientRect` we want to calculate.
|
||||
*
|
||||
* @return {Object|undefined}
|
||||
* Always returns a plain
|
||||
*/
|
||||
export function getBoundingClientRect(el) {
|
||||
if (el.getBoundingClientRect && el.parentNode) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const result = {};
|
||||
|
||||
['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(k => {
|
||||
if (rect[k] !== undefined) {
|
||||
result[k] = rect[k];
|
||||
}
|
||||
});
|
||||
|
||||
if (!result.height) {
|
||||
result.height = parseFloat(computedStyle(el, 'height'));
|
||||
}
|
||||
|
||||
if (!result.width) {
|
||||
result.width = parseFloat(computedStyle(el, 'width'));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The postion of a DOM element on the page.
|
||||
*
|
||||
@@ -599,7 +531,7 @@ export function unblockTextSelection() {
|
||||
* @return {Dom~Position}
|
||||
* The position of the element that was passed in.
|
||||
*/
|
||||
export function findElPosition(el) {
|
||||
export function findPosition(el) {
|
||||
let box;
|
||||
|
||||
if (el.getBoundingClientRect && el.parentNode) {
|
||||
@@ -660,7 +592,7 @@ export function findElPosition(el) {
|
||||
*/
|
||||
export function getPointerPosition(el, event) {
|
||||
const position = {};
|
||||
const box = findElPosition(el);
|
||||
const box = findPosition(el);
|
||||
const boxW = el.offsetWidth;
|
||||
const boxH = el.offsetHeight;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @module events
|
||||
*/
|
||||
|
||||
import * as Dom from './dom.js';
|
||||
import * as DomData from './dom-data';
|
||||
import * as Guid from './guid.js';
|
||||
import log from './log.js';
|
||||
import window from 'global/window';
|
||||
@@ -23,7 +23,7 @@ import document from 'global/document';
|
||||
* Type of event to clean up
|
||||
*/
|
||||
function _cleanUpEvents(elem, type) {
|
||||
const data = Dom.getElData(elem);
|
||||
const data = DomData.getData(elem);
|
||||
|
||||
// Remove the events of a particular type if there are none left
|
||||
if (data.handlers[type].length === 0) {
|
||||
@@ -48,7 +48,7 @@ function _cleanUpEvents(elem, type) {
|
||||
|
||||
// Finally remove the element data if there is no data left
|
||||
if (Object.getOwnPropertyNames(data).length === 0) {
|
||||
Dom.removeElData(elem);
|
||||
DomData.removeData(elem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ export function on(elem, type, fn) {
|
||||
return _handleMultipleEvents(on, elem, type, fn);
|
||||
}
|
||||
|
||||
const data = Dom.getElData(elem);
|
||||
const data = DomData.getData(elem);
|
||||
|
||||
// We need a place to store all our handler data
|
||||
if (!data.handlers) {
|
||||
@@ -295,11 +295,11 @@ export function on(elem, type, fn) {
|
||||
*/
|
||||
export function off(elem, type, fn) {
|
||||
// Don't want to add a cache object through getElData if not needed
|
||||
if (!Dom.hasElData(elem)) {
|
||||
if (!DomData.hasData(elem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = Dom.getElData(elem);
|
||||
const data = DomData.getData(elem);
|
||||
|
||||
// If no events exist, nothing to unbind
|
||||
if (!data.handlers) {
|
||||
@@ -369,7 +369,7 @@ export function trigger(elem, event, hash) {
|
||||
// Fetches element data and a reference to the parent (for bubbling).
|
||||
// Don't want to add a data object to cache for every parent,
|
||||
// so checking hasElData first.
|
||||
const elemData = (Dom.hasElData(elem)) ? Dom.getElData(elem) : {};
|
||||
const elemData = (DomData.hasData(elem)) ? DomData.getData(elem) : {};
|
||||
const parent = elem.parentNode || elem.ownerDocument;
|
||||
// type = event.type || event,
|
||||
// handler;
|
||||
@@ -393,7 +393,7 @@ export function trigger(elem, event, hash) {
|
||||
|
||||
// If at the top of the DOM, triggers the default action unless disabled.
|
||||
} else if (!parent && !event.defaultPrevented) {
|
||||
const targetData = Dom.getElData(event.target);
|
||||
const targetData = DomData.getData(event.target);
|
||||
|
||||
// Checks if the target has a default action for this event.
|
||||
if (event.target[event.type]) {
|
||||
|
||||
+107
-15
@@ -8,9 +8,16 @@ import {isObject} from './obj';
|
||||
|
||||
let log;
|
||||
|
||||
// This is the private tracking variable for logging level.
|
||||
let level = 'all';
|
||||
|
||||
// This is the private tracking variable for the logging history.
|
||||
let history = [];
|
||||
|
||||
/**
|
||||
* Log messages to the console and history based on the type of message
|
||||
*
|
||||
* @private
|
||||
* @param {string} type
|
||||
* The name of the console method to use.
|
||||
*
|
||||
@@ -22,29 +29,34 @@ let log;
|
||||
* but this is exposed as a parameter to facilitate testing.
|
||||
*/
|
||||
export const logByType = (type, args, stringify = !!IE_VERSION && IE_VERSION < 11) => {
|
||||
const lvl = log.levels[level];
|
||||
const lvlRegExp = new RegExp(`^(${lvl})$`);
|
||||
|
||||
if (type !== 'log') {
|
||||
|
||||
// add the type to the front of the message when it's not "log"
|
||||
// Add the type to the front of the message when it's not "log".
|
||||
args.unshift(type.toUpperCase() + ':');
|
||||
}
|
||||
|
||||
// add to history
|
||||
log.history.push(args);
|
||||
// Add a clone of the args at this point to history.
|
||||
if (history) {
|
||||
history.push([].concat(args));
|
||||
}
|
||||
|
||||
// add console prefix after adding to history
|
||||
// Add console prefix after adding to history.
|
||||
args.unshift('VIDEOJS:');
|
||||
|
||||
// If there's no console then don't try to output messages, but they will
|
||||
// still be stored in `log.history`.
|
||||
// still be stored in history.
|
||||
//
|
||||
// Was setting these once outside of this function, but containing them
|
||||
// in the function makes it easier to test cases where console doesn't exist
|
||||
// when the module is executed.
|
||||
const fn = window.console && window.console[type];
|
||||
|
||||
// Bail out if there's no console.
|
||||
if (!fn) {
|
||||
// Bail out if there's no console or if this type is not allowed by the
|
||||
// current logging level.
|
||||
if (!fn || !lvl || !lvlRegExp.test(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,24 +88,104 @@ export const logByType = (type, args, stringify = !!IE_VERSION && IE_VERSION < 1
|
||||
};
|
||||
|
||||
/**
|
||||
* Log plain debug messages
|
||||
* Logs plain debug messages. Similar to `console.log`.
|
||||
*
|
||||
* @param {Mixed[]} args
|
||||
* One or more messages or objects that should be logged.
|
||||
* @class
|
||||
* @param {Mixed[]} args
|
||||
* One or more messages or objects that should be logged.
|
||||
*/
|
||||
log = function(...args) {
|
||||
logByType('log', args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Keep a history of log messages
|
||||
* Enumeration of available logging levels, where the keys are the level names
|
||||
* and the values are `|`-separated strings containing logging methods allowed
|
||||
* in that logging level. These strings are used to create a regular expression
|
||||
* matching the function name being called.
|
||||
*
|
||||
* @type {Array}
|
||||
* Levels provided by video.js are:
|
||||
*
|
||||
* - `off`: Matches no calls. Any value that can be cast to `false` will have
|
||||
* this effect. The most restrictive.
|
||||
* - `all` (default): Matches only Video.js-provided functions (`log`,
|
||||
* `log.warn`, and `log.error`).
|
||||
* - `warn`: Matches `log.warn` and `log.error` calls.
|
||||
* - `error`: Matches only `log.error` calls.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
log.history = [];
|
||||
log.levels = {
|
||||
all: 'log|warn|error',
|
||||
error: 'error',
|
||||
off: '',
|
||||
warn: 'warn|error',
|
||||
DEFAULT: level
|
||||
};
|
||||
|
||||
/**
|
||||
* Log error messages
|
||||
* Get or set the current logging level. If a string matching a key from
|
||||
* {@link log.levels} is provided, acts as a setter. Regardless of argument,
|
||||
* returns the current logging level.
|
||||
*
|
||||
* @param {string} [lvl]
|
||||
* Pass to set a new logging level.
|
||||
*
|
||||
* @return {string}
|
||||
* The current logging level.
|
||||
*/
|
||||
log.level = (lvl) => {
|
||||
if (typeof lvl === 'string') {
|
||||
if (!log.levels.hasOwnProperty(lvl)) {
|
||||
throw new Error(`"${lvl}" in not a valid log level`);
|
||||
}
|
||||
level = lvl;
|
||||
}
|
||||
return level;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array containing everything that has been logged to the history.
|
||||
*
|
||||
* This array is a shallow clone of the internal history record. However, its
|
||||
* contents are _not_ cloned; so, mutating objects inside this array will
|
||||
* mutate them in history.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
log.history = () => history ? [].concat(history) : [];
|
||||
|
||||
/**
|
||||
* Clears the internal history tracking, but does not prevent further history
|
||||
* tracking.
|
||||
*/
|
||||
log.history.clear = () => {
|
||||
if (history) {
|
||||
history.length = 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Disable history tracking if it is currently enabled.
|
||||
*/
|
||||
log.history.disable = () => {
|
||||
if (history !== null) {
|
||||
history.length = 0;
|
||||
history = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable history tracking if it is currently disabled.
|
||||
*/
|
||||
log.history.enable = () => {
|
||||
if (history === null) {
|
||||
history = [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Logs error messages. Similar to `console.error`.
|
||||
*
|
||||
* @param {Mixed[]} args
|
||||
* One or more messages or objects that should be logged as an error
|
||||
@@ -101,7 +193,7 @@ log.history = [];
|
||||
log.error = (...args) => logByType('error', args);
|
||||
|
||||
/**
|
||||
* Log warning messages
|
||||
* Logs warning messages. Similar to `console.warn`.
|
||||
*
|
||||
* @param {Mixed[]} args
|
||||
* One or more messages or objects that should be logged as a warning.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* @file time-ranges.js
|
||||
* @module time-ranges
|
||||
*/
|
||||
import log from './log.js';
|
||||
|
||||
/**
|
||||
* Returns the time for the specified index at the start or end
|
||||
@@ -51,8 +50,8 @@ import log from './log.js';
|
||||
* @throws {Error} if the timeRanges provided are over the maxIndex
|
||||
*/
|
||||
function rangeCheck(fnName, index, maxIndex) {
|
||||
if (index < 0 || index > maxIndex) {
|
||||
throw new Error(`Failed to execute '${fnName}' on 'TimeRanges': The index provided (${index}) is greater than or equal to the maximum bound (${maxIndex}).`);
|
||||
if (typeof index !== 'number' || index < 0 || index > maxIndex) {
|
||||
throw new Error(`Failed to execute '${fnName}' on 'TimeRanges': The index provided (${index}) is non-numeric or out of bounds (0-${maxIndex}).`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,10 +78,6 @@ function rangeCheck(fnName, index, maxIndex) {
|
||||
* @throws {Error} if rangeIndex is more than the length of ranges
|
||||
*/
|
||||
function getRange(fnName, valueIndex, ranges, rangeIndex) {
|
||||
if (rangeIndex === undefined) {
|
||||
log.warn(`DEPRECATED: Function '${fnName}' on 'TimeRanges' called without an index argument.`);
|
||||
rangeIndex = 0;
|
||||
}
|
||||
rangeCheck(fnName, rangeIndex, ranges.length - 1);
|
||||
return ranges[rangeIndex][valueIndex];
|
||||
}
|
||||
|
||||
+123
-40
@@ -2,9 +2,6 @@
|
||||
* @file video.js
|
||||
* @module videojs
|
||||
*/
|
||||
|
||||
/* global define */
|
||||
|
||||
import window from 'global/window';
|
||||
import document from 'global/document';
|
||||
import * as setup from './setup';
|
||||
@@ -13,7 +10,7 @@ import Component from './component';
|
||||
import EventTarget from './event-target';
|
||||
import * as Events from './utils/events.js';
|
||||
import Player from './player';
|
||||
import plugin from './plugins.js';
|
||||
import Plugin from './plugin';
|
||||
import mergeOptions from './utils/merge-options.js';
|
||||
import * as Fn from './utils/fn.js';
|
||||
import TextTrack from './tracks/text-track.js';
|
||||
@@ -33,6 +30,7 @@ import xhr from 'xhr';
|
||||
|
||||
// Include the built-in techs
|
||||
import Tech from './tech/tech.js';
|
||||
import { use as middlewareUse } from './tech/middleware.js';
|
||||
|
||||
// HTML5 Element Shim for IE8
|
||||
if (typeof HTMLVideoElement === 'undefined' && Dom.isReal()) {
|
||||
@@ -66,6 +64,7 @@ function videojs(id, options, ready) {
|
||||
// Allow for element or ID to be passed in
|
||||
// String ID
|
||||
if (typeof id === 'string') {
|
||||
const players = videojs.getPlayers();
|
||||
|
||||
// Adjust for jQuery ID syntax
|
||||
if (id.indexOf('#') === 0) {
|
||||
@@ -73,22 +72,22 @@ function videojs(id, options, ready) {
|
||||
}
|
||||
|
||||
// If a player instance has already been created for this ID return it.
|
||||
if (videojs.getPlayers()[id]) {
|
||||
if (players[id]) {
|
||||
|
||||
// If options or ready funtion are passed, warn
|
||||
// If options or ready function are passed, warn
|
||||
if (options) {
|
||||
log.warn(`Player "${id}" is already initialised. Options will not be applied.`);
|
||||
}
|
||||
|
||||
if (ready) {
|
||||
videojs.getPlayers()[id].ready(ready);
|
||||
players[id].ready(ready);
|
||||
}
|
||||
|
||||
return videojs.getPlayers()[id];
|
||||
return players[id];
|
||||
}
|
||||
|
||||
// Otherwise get element for ID
|
||||
tag = Dom.getEl(id);
|
||||
tag = Dom.$('#' + id);
|
||||
|
||||
// ID is a media element
|
||||
} else {
|
||||
@@ -300,6 +299,8 @@ videojs.getTech = Tech.getTech;
|
||||
*/
|
||||
videojs.registerTech = Tech.registerTech;
|
||||
|
||||
videojs.use = middlewareUse;
|
||||
|
||||
/**
|
||||
* A suite of browser and device tests from {@link browser}.
|
||||
*
|
||||
@@ -347,14 +348,77 @@ videojs.mergeOptions = mergeOptions;
|
||||
videojs.bind = Fn.bind;
|
||||
|
||||
/**
|
||||
* Create a Video.js player plugin.
|
||||
* Plugins are only initialized when options for the plugin are included
|
||||
* in the player options, or the plugin function on the player instance is
|
||||
* called.
|
||||
* Register a Video.js plugin.
|
||||
*
|
||||
* @borrows plugin:plugin as videojs.plugin
|
||||
* @borrows plugin:registerPlugin as videojs.registerPlugin
|
||||
* @mixes videojs
|
||||
* @method registerPlugin
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of the plugin to be registered. Must be a string and
|
||||
* must not match an existing plugin or a method on the `Player`
|
||||
* prototype.
|
||||
*
|
||||
* @param {Function} plugin
|
||||
* A sub-class of `Plugin` or a function for basic plugins.
|
||||
*
|
||||
* @return {Function}
|
||||
* For advanced plugins, a factory function for that plugin. For
|
||||
* basic plugins, a wrapper function that initializes the plugin.
|
||||
*/
|
||||
videojs.plugin = plugin;
|
||||
videojs.registerPlugin = Plugin.registerPlugin;
|
||||
|
||||
/**
|
||||
* Deprecated method to register a plugin with Video.js
|
||||
*
|
||||
* @deprecated
|
||||
* videojs.plugin() is deprecated; use videojs.registerPlugin() instead
|
||||
*
|
||||
* @param {string} name
|
||||
* The plugin name
|
||||
*
|
||||
* @param {Plugin|Function} plugin
|
||||
* The plugin sub-class or function
|
||||
*/
|
||||
videojs.plugin = (name, plugin) => {
|
||||
log.warn('videojs.plugin() is deprecated; use videojs.registerPlugin() instead');
|
||||
return Plugin.registerPlugin(name, plugin);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an object containing multiple Video.js plugins.
|
||||
*
|
||||
* @param {Array} [names]
|
||||
* If provided, should be an array of plugin names. Defaults to _all_
|
||||
* plugin names.
|
||||
*
|
||||
* @return {Object|undefined}
|
||||
* An object containing plugin(s) associated with their name(s) or
|
||||
* `undefined` if no matching plugins exist).
|
||||
*/
|
||||
videojs.getPlugins = Plugin.getPlugins;
|
||||
|
||||
/**
|
||||
* Gets a plugin by name if it exists.
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of a plugin.
|
||||
*
|
||||
* @return {Function|undefined}
|
||||
* The plugin (or `undefined`).
|
||||
*/
|
||||
videojs.getPlugin = Plugin.getPlugin;
|
||||
|
||||
/**
|
||||
* Gets a plugin's version, if available
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of a plugin.
|
||||
*
|
||||
* @return {string}
|
||||
* The plugin's version or an empty string.
|
||||
*/
|
||||
videojs.getPluginVersion = Plugin.getPluginVersion;
|
||||
|
||||
/**
|
||||
* Adding languages so that they're available to all players.
|
||||
@@ -498,58 +562,58 @@ videojs.VideoTrack = VideoTrack;
|
||||
* Determines, via duck typing, whether or not a value is a DOM element.
|
||||
*
|
||||
* @borrows dom:isEl as videojs.isEl
|
||||
* @deprecated Use videojs.dom.isEl() instead
|
||||
*/
|
||||
videojs.isEl = Dom.isEl;
|
||||
|
||||
/**
|
||||
* Determines, via duck typing, whether or not a value is a text node.
|
||||
*
|
||||
* @borrows dom:isTextNode as videojs.isTextNode
|
||||
* @deprecated Use videojs.dom.isTextNode() instead
|
||||
*/
|
||||
videojs.isTextNode = Dom.isTextNode;
|
||||
|
||||
/**
|
||||
* Creates an element and applies properties.
|
||||
*
|
||||
* @borrows dom:createEl as videojs.createEl
|
||||
* @deprecated Use videojs.dom.createEl() instead
|
||||
*/
|
||||
videojs.createEl = Dom.createEl;
|
||||
|
||||
/**
|
||||
* Check if an element has a CSS class
|
||||
*
|
||||
* @borrows dom:hasElClass as videojs.hasClass
|
||||
* @deprecated Use videojs.dom.hasClass() instead
|
||||
*/
|
||||
videojs.hasClass = Dom.hasElClass;
|
||||
|
||||
/**
|
||||
* Add a CSS class name to an element
|
||||
*
|
||||
* @borrows dom:addElClass as videojs.addClass
|
||||
* @deprecated Use videojs.dom.addClass() instead
|
||||
*/
|
||||
videojs.addClass = Dom.addElClass;
|
||||
|
||||
/**
|
||||
* Remove a CSS class name from an element
|
||||
*
|
||||
* @borrows dom:removeElClass as videojs.removeClass
|
||||
* @deprecated Use videojs.dom.removeClass() instead
|
||||
*/
|
||||
videojs.removeClass = Dom.removeElClass;
|
||||
|
||||
/**
|
||||
* Adds or removes a CSS class name on an element depending on an optional
|
||||
* condition or the presence/absence of the class name.
|
||||
*
|
||||
* @borrows dom:toggleElClass as videojs.toggleClass
|
||||
* @deprecated Use videojs.dom.toggleClass() instead
|
||||
*/
|
||||
videojs.toggleClass = Dom.toggleElClass;
|
||||
|
||||
/**
|
||||
* Apply attributes to an HTML element.
|
||||
*
|
||||
* @borrows dom:setElAttributes as videojs.setAttribute
|
||||
* @deprecated Use videojs.dom.setAttributes() instead
|
||||
*/
|
||||
videojs.setAttributes = Dom.setElAttributes;
|
||||
|
||||
/**
|
||||
* Get an element's attribute values, as defined on the HTML tag
|
||||
@@ -558,15 +622,15 @@ videojs.setAttributes = Dom.setElAttributes;
|
||||
* This will return true or false for boolean attributes.
|
||||
*
|
||||
* @borrows dom:getElAttributes as videojs.getAttributes
|
||||
* @deprecated Use videojs.dom.getAttributes() instead
|
||||
*/
|
||||
videojs.getAttributes = Dom.getElAttributes;
|
||||
|
||||
/**
|
||||
* Empties the contents of an element.
|
||||
*
|
||||
* @borrows dom:emptyEl as videojs.emptyEl
|
||||
* @deprecated Use videojs.dom.emptyEl() instead
|
||||
*/
|
||||
videojs.emptyEl = Dom.emptyEl;
|
||||
|
||||
/**
|
||||
* Normalizes and appends content to an element.
|
||||
@@ -589,8 +653,8 @@ videojs.emptyEl = Dom.emptyEl;
|
||||
* node, or array.
|
||||
*
|
||||
* @borrows dom:appendContents as videojs.appendContet
|
||||
* @deprecated Use videojs.dom.appendContent() instead
|
||||
*/
|
||||
videojs.appendContent = Dom.appendContent;
|
||||
|
||||
/**
|
||||
* Normalizes and inserts content into an element; this is identical to
|
||||
@@ -614,8 +678,27 @@ videojs.appendContent = Dom.appendContent;
|
||||
* node, or array.
|
||||
*
|
||||
* @borrows dom:insertContent as videojs.insertContent
|
||||
* @deprecated Use videojs.dom.insertContent() instead
|
||||
*/
|
||||
videojs.insertContent = Dom.insertContent;
|
||||
[
|
||||
'isEl',
|
||||
'isTextNode',
|
||||
'createEl',
|
||||
'hasClass',
|
||||
'addClass',
|
||||
'removeClass',
|
||||
'toggleClass',
|
||||
'setAttributes',
|
||||
'getAttributes',
|
||||
'emptyEl',
|
||||
'appendContent',
|
||||
'insertContent'
|
||||
].forEach(k => {
|
||||
videojs[k] = function() {
|
||||
log.warn(`videojs.${k}() is deprecated; use videojs.dom.${k}() instead`);
|
||||
return Dom[k].apply(null, arguments);
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* A safe getComputedStyle with an IE8 fallback.
|
||||
@@ -629,19 +712,19 @@ videojs.insertContent = Dom.insertContent;
|
||||
*/
|
||||
videojs.computedStyle = computedStyle;
|
||||
|
||||
/*
|
||||
* Custom Universal Module Definition (UMD)
|
||||
*
|
||||
* Video.js will never be a non-browser lib so we can simplify UMD a bunch and
|
||||
* still support requirejs and browserify. This also needs to be closure
|
||||
* compiler compatible, so string keys are used.
|
||||
/**
|
||||
* Export the Dom utilities for use in external plugins
|
||||
* and Tech's
|
||||
*/
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('videojs', [], () => videojs);
|
||||
videojs.dom = Dom;
|
||||
|
||||
// checking that module is an object too because of umdjs/umd#35
|
||||
} else if (typeof exports === 'object' && typeof module === 'object') {
|
||||
module.exports = videojs;
|
||||
}
|
||||
/**
|
||||
* Export the Url utilities for use in external plugins
|
||||
* and Tech's
|
||||
*/
|
||||
videojs.url = Url;
|
||||
|
||||
// We use Node-style module.exports here instead of ES6 because it is more
|
||||
// compatible with different module systems.
|
||||
module.exports = videojs;
|
||||
|
||||
export default videojs;
|
||||
|
||||
@@ -124,12 +124,9 @@ QUnit.test('should be able to access expected MediaTech API methods', function(a
|
||||
const mediaProto = media.prototype;
|
||||
const html5 = videojs.getComponent('Html5');
|
||||
const html5Proto = html5.prototype;
|
||||
const flash = videojs.getComponent('Flash');
|
||||
const flashProto = flash.prototype;
|
||||
|
||||
assert.ok(mediaProto.setPoster, 'setPoster should exist on the Media tech');
|
||||
assert.ok(html5Proto.setPoster, 'setPoster should exist on the HTML5 tech');
|
||||
assert.ok(flashProto.setPoster, 'setPoster should exist on the Flash tech');
|
||||
|
||||
assert.ok(html5.patchCanPlayType, 'patchCanPlayType should exist for HTML5');
|
||||
assert.ok(html5.unpatchCanPlayType, 'unpatchCanPlayType should exist for HTML5');
|
||||
@@ -143,13 +140,6 @@ QUnit.test('should be able to access expected MediaTech API methods', function(a
|
||||
assert.ok(html5.prototype.setSource, 'setSource should exist for Html5');
|
||||
assert.ok(html5.prototype.disposeSourceHandler,
|
||||
'disposeSourceHandler should exist for Html5');
|
||||
|
||||
assert.ok(flash.canPlaySource, 'canPlaySource should exist for Flash');
|
||||
assert.ok(flash.registerSourceHandler, 'registerSourceHandler should exist for Flash');
|
||||
assert.ok(flash.selectSourceHandler, 'selectSourceHandler should exist for Flash');
|
||||
assert.ok(flash.prototype.setSource, 'setSource should exist for Flash');
|
||||
assert.ok(flash.prototype.disposeSourceHandler,
|
||||
'disposeSourceHandler should exist for Flash');
|
||||
});
|
||||
|
||||
QUnit.test('should export ready api call to public', function(assert) {
|
||||
|
||||
@@ -9,13 +9,11 @@
|
||||
<body>
|
||||
<div id="qunit"></div>
|
||||
<script src="../node_modules/qunitjs/qunit/qunit.js"></script>
|
||||
|
||||
<script src="../build/temp/ie8/videojs-ie8.js"></script>
|
||||
|
||||
<!-- Execute the bundled tests first -->
|
||||
<script src="../build/temp/tests.js"></script>
|
||||
<script src="../build/temp/browserify.js"></script>
|
||||
<script src="../build/temp/webpack.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/* eslint-env qunit */
|
||||
import window from 'global/window';
|
||||
import Component from '../../src/js/component.js';
|
||||
import * as Dom from '../../src/js/utils/dom.js';
|
||||
import * as DomData from '../../src/js/utils/dom-data';
|
||||
import * as Events from '../../src/js/utils/events.js';
|
||||
import * as Obj from '../../src/js/utils/obj';
|
||||
import * as browser from '../../src/js/utils/browser.js';
|
||||
import document from 'global/document';
|
||||
import sinon from 'sinon';
|
||||
@@ -41,6 +44,44 @@ const getFakePlayer = function() {
|
||||
};
|
||||
};
|
||||
|
||||
QUnit.test('registerComponent() throws with bad arguments', function(assert) {
|
||||
assert.throws(
|
||||
function() {
|
||||
Component.registerComponent(null);
|
||||
},
|
||||
new Error('Illegal component name, "null"; must be a non-empty string.'),
|
||||
'component names must be non-empty strings'
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
function() {
|
||||
Component.registerComponent('');
|
||||
},
|
||||
new Error('Illegal component name, ""; must be a non-empty string.'),
|
||||
'component names must be non-empty strings'
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
function() {
|
||||
Component.registerComponent('TestComponent5', function() {});
|
||||
},
|
||||
new Error('Illegal component, "TestComponent5"; must be a Component subclass.'),
|
||||
'components must be subclasses of Component'
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
function() {
|
||||
const Tech = Component.getComponent('Tech');
|
||||
|
||||
class DummyTech extends Tech {}
|
||||
|
||||
Component.registerComponent('TestComponent5', DummyTech);
|
||||
},
|
||||
new Error('Illegal component, "TestComponent5"; techs must be registered using Tech.registerTech().'),
|
||||
'components must be subclasses of Component'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('should create an element', function(assert) {
|
||||
const comp = new Component(getFakePlayer(), {});
|
||||
|
||||
@@ -255,7 +296,7 @@ QUnit.test('should dispose of component and children', function(assert) {
|
||||
return true;
|
||||
});
|
||||
const el = comp.el();
|
||||
const data = Dom.getElData(el);
|
||||
const data = DomData.getData(el);
|
||||
|
||||
let hasDisposed = false;
|
||||
let bubbles = null;
|
||||
@@ -273,7 +314,7 @@ QUnit.test('should dispose of component and children', function(assert) {
|
||||
assert.ok(!comp.el(), 'component element was deleted');
|
||||
assert.ok(!child.children(), 'child children were deleted');
|
||||
assert.ok(!child.el(), 'child element was deleted');
|
||||
assert.ok(!Dom.hasElData(el), 'listener data nulled');
|
||||
assert.ok(!DomData.hasData(el), 'listener data nulled');
|
||||
assert.ok(!Object.getOwnPropertyNames(data).length,
|
||||
'original listener data object was emptied');
|
||||
});
|
||||
@@ -557,13 +598,13 @@ QUnit.test('dimension() should treat NaN and null as zero', function(assert) {
|
||||
newWidth = comp.dimension('width', null);
|
||||
|
||||
assert.notEqual(newWidth, width, 'new width and old width are not the same');
|
||||
assert.equal(newWidth, comp, 'we set a value, so, return value is component');
|
||||
assert.equal(newWidth, undefined, 'we set a value, so, return value is undefined');
|
||||
assert.equal(comp.width(), 0, 'the new width is zero');
|
||||
|
||||
const newHeight = comp.dimension('height', NaN);
|
||||
|
||||
assert.notEqual(newHeight, height, 'new height and old height are not the same');
|
||||
assert.equal(newHeight, comp, 'we set a value, so, return value is component');
|
||||
assert.equal(newHeight, undefined, 'we set a value, so, return value is undefined');
|
||||
assert.equal(comp.height(), 0, 'the new height is zero');
|
||||
|
||||
comp.width(width);
|
||||
@@ -635,7 +676,7 @@ QUnit.test('should use a defined content el for appending children', function(as
|
||||
class CompWithContent extends Component {}
|
||||
|
||||
CompWithContent.prototype.createEl = function() {
|
||||
// Create the main componenent element
|
||||
// Create the main component element
|
||||
const el = Dom.createEl('div');
|
||||
|
||||
// Create the element where children will be appended
|
||||
@@ -788,6 +829,70 @@ QUnit.test('should provide interval methods that automatically get cleared on co
|
||||
assert.ok(intervalsFired === 5, 'Interval was cleared when component was disposed');
|
||||
});
|
||||
|
||||
QUnit.test('should provide *AnimationFrame methods that automatically get cleared on component disposal', function(assert) {
|
||||
const comp = new Component(getFakePlayer());
|
||||
const oldRAF = window.requestAnimationFrame;
|
||||
const oldCAF = window.cancelAnimationFrame;
|
||||
|
||||
// Stub the window.*AnimationFrame methods with window.setTimeout methods
|
||||
// so we can control when the callbacks are called via sinon's timer stubs.
|
||||
window.requestAnimationFrame = (fn) => window.setTimeout(fn, 1);
|
||||
window.cancelAnimationFrame = (id) => window.clearTimeout(id);
|
||||
|
||||
// Make sure the component thinks it supports rAF.
|
||||
comp.supportsRaf_ = true;
|
||||
|
||||
const spyRAF = sinon.spy();
|
||||
|
||||
comp.requestAnimationFrame(spyRAF);
|
||||
|
||||
assert.strictEqual(spyRAF.callCount, 0, 'rAF callback was not called immediately');
|
||||
this.clock.tick(1);
|
||||
assert.strictEqual(spyRAF.callCount, 1, 'rAF callback was called after a "repaint"');
|
||||
this.clock.tick(1);
|
||||
assert.strictEqual(spyRAF.callCount, 1, 'rAF callback was not called after a second "repaint"');
|
||||
|
||||
comp.cancelAnimationFrame(comp.requestAnimationFrame(spyRAF));
|
||||
this.clock.tick(1);
|
||||
assert.strictEqual(spyRAF.callCount, 1, 'second rAF callback was not called because it was cancelled');
|
||||
|
||||
comp.requestAnimationFrame(spyRAF);
|
||||
comp.dispose();
|
||||
this.clock.tick(1);
|
||||
assert.strictEqual(spyRAF.callCount, 1, 'third rAF callback was not called because the component was disposed');
|
||||
|
||||
window.requestAnimationFrame = oldRAF;
|
||||
window.cancelAnimationFrame = oldCAF;
|
||||
});
|
||||
|
||||
QUnit.test('*AnimationFrame methods fall back to timers if rAF not supported', function(assert) {
|
||||
const comp = new Component(getFakePlayer());
|
||||
const oldRAF = window.requestAnimationFrame;
|
||||
const oldCAF = window.cancelAnimationFrame;
|
||||
|
||||
// Stub the window.*AnimationFrame methods with window.setTimeout methods
|
||||
// so we can control when the callbacks are called via sinon's timer stubs.
|
||||
const rAF = window.requestAnimationFrame = sinon.spy();
|
||||
const cAF = window.cancelAnimationFrame = sinon.spy();
|
||||
|
||||
// Make sure the component thinks it does not support rAF.
|
||||
comp.supportsRaf_ = false;
|
||||
|
||||
sinon.spy(comp, 'setTimeout');
|
||||
sinon.spy(comp, 'clearTimeout');
|
||||
|
||||
comp.cancelAnimationFrame(comp.requestAnimationFrame(() => {}));
|
||||
|
||||
assert.strictEqual(rAF.callCount, 0, 'window.requestAnimationFrame was not called');
|
||||
assert.strictEqual(cAF.callCount, 0, 'window.cancelAnimationFrame was not called');
|
||||
assert.strictEqual(comp.setTimeout.callCount, 1, 'Component#setTimeout was called');
|
||||
assert.strictEqual(comp.clearTimeout.callCount, 1, 'Component#clearTimeout was called');
|
||||
|
||||
comp.dispose();
|
||||
window.requestAnimationFrame = oldRAF;
|
||||
window.cancelAnimationFrame = oldCAF;
|
||||
});
|
||||
|
||||
QUnit.test('$ and $$ functions', function(assert) {
|
||||
const comp = new Component(getFakePlayer());
|
||||
const contentEl = document.createElement('div');
|
||||
@@ -802,3 +907,13 @@ QUnit.test('$ and $$ functions', function(assert) {
|
||||
assert.strictEqual(comp.$('div'), children[0], '$ defaults to contentEl as scope');
|
||||
assert.strictEqual(comp.$$('div').length, children.length, '$$ defaults to contentEl as scope');
|
||||
});
|
||||
|
||||
QUnit.test('should use the stateful mixin', function(assert) {
|
||||
const comp = new Component(getFakePlayer(), {});
|
||||
|
||||
assert.ok(Obj.isPlain(comp.state), '`state` is a plain object');
|
||||
assert.strictEqual(Object.prototype.toString.call(comp.setState), '[object Function]', '`setState` is a function');
|
||||
|
||||
comp.setState({foo: 'bar'});
|
||||
assert.strictEqual(comp.state.foo, 'bar', 'the component passes a basic stateful test');
|
||||
});
|
||||
|
||||
@@ -6,54 +6,29 @@ import Slider from '../../src/js/slider/slider.js';
|
||||
import FullscreenToggle from '../../src/js/control-bar/fullscreen-toggle.js';
|
||||
import TestHelpers from './test-helpers.js';
|
||||
import document from 'global/document';
|
||||
import Html5 from '../../src/js/tech/html5.js';
|
||||
|
||||
QUnit.module('Controls');
|
||||
|
||||
QUnit.test('should hide volume control if it\'s not supported', function(assert) {
|
||||
assert.expect(2);
|
||||
const noop = function() {};
|
||||
const player = {
|
||||
id: noop,
|
||||
on: noop,
|
||||
ready: noop,
|
||||
tech_: {
|
||||
featuresVolumeControl: false
|
||||
},
|
||||
volume() {},
|
||||
muted() {},
|
||||
reportUserActivity() {}
|
||||
};
|
||||
|
||||
const player = TestHelpers.makePlayer();
|
||||
|
||||
player.tech_.featuresVolumeControl = false;
|
||||
|
||||
const volumeControl = new VolumeControl(player);
|
||||
const muteToggle = new MuteToggle(player);
|
||||
|
||||
assert.ok(volumeControl.el().className.indexOf('vjs-hidden') >= 0, 'volumeControl is not hidden');
|
||||
assert.ok(muteToggle.el().className.indexOf('vjs-hidden') >= 0, 'muteToggle is not hidden');
|
||||
assert.ok(volumeControl.hasClass('vjs-hidden'), 'volumeControl is not hidden');
|
||||
assert.ok(muteToggle.hasClass('vjs-hidden'), 'muteToggle is not hidden');
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('should test and toggle volume control on `loadstart`', function(assert) {
|
||||
const noop = function() {};
|
||||
const listeners = [];
|
||||
const player = {
|
||||
id: noop,
|
||||
on(event, callback) {
|
||||
// don't fire dispose listeners
|
||||
if (event !== 'dispose') {
|
||||
listeners.push(callback);
|
||||
}
|
||||
},
|
||||
ready: noop,
|
||||
volume() {
|
||||
return 1;
|
||||
},
|
||||
muted() {
|
||||
return false;
|
||||
},
|
||||
tech_: {
|
||||
featuresVolumeControl: true
|
||||
},
|
||||
reportUserActivity() {}
|
||||
};
|
||||
const player = TestHelpers.makePlayer();
|
||||
|
||||
player.tech_.featuresVolumeControl = true;
|
||||
|
||||
const volumeControl = new VolumeControl(player);
|
||||
const muteToggle = new MuteToggle(player);
|
||||
@@ -62,30 +37,23 @@ QUnit.test('should test and toggle volume control on `loadstart`', function(asse
|
||||
assert.equal(muteToggle.hasClass('vjs-hidden'), false, 'muteToggle is hidden initially');
|
||||
|
||||
player.tech_.featuresVolumeControl = false;
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
listeners[i]();
|
||||
}
|
||||
player.trigger('loadstart');
|
||||
|
||||
assert.equal(volumeControl.hasClass('vjs-hidden'), true, 'volumeControl does not hide itself');
|
||||
assert.equal(muteToggle.hasClass('vjs-hidden'), true, 'muteToggle does not hide itself');
|
||||
|
||||
player.tech_.featuresVolumeControl = true;
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
listeners[i]();
|
||||
}
|
||||
player.trigger('loadstart');
|
||||
|
||||
assert.equal(volumeControl.hasClass('vjs-hidden'), false, 'volumeControl does not show itself');
|
||||
assert.equal(muteToggle.hasClass('vjs-hidden'), false, 'muteToggle does not show itself');
|
||||
});
|
||||
|
||||
QUnit.test('calculateDistance should use changedTouches, if available', function(assert) {
|
||||
const noop = function() {};
|
||||
const player = {
|
||||
id: noop,
|
||||
on: noop,
|
||||
ready: noop,
|
||||
reportUserActivity: noop
|
||||
};
|
||||
const player = TestHelpers.makePlayer();
|
||||
|
||||
player.tech_.featuresVolumeControl = true;
|
||||
|
||||
const slider = new Slider(player);
|
||||
|
||||
document.body.appendChild(slider.el_);
|
||||
@@ -125,3 +93,49 @@ QUnit.test('Fullscreen control text should be correct when fullscreenchange is t
|
||||
QUnit.equal(fullscreentoggle.controlText(), 'Fullscreen', 'Control Text is correct while switching back to normal mode');
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
// only run these tests if Html5 is supported.
|
||||
// IE8 can't run the Html5 tech and to test this functionality otherwith the the tech faker,
|
||||
// we'd need to implement volume functionality into tech faker
|
||||
if (Html5.isSupported()) {
|
||||
QUnit.test('Clicking MuteToggle when volume is above 0 should toggle muted property and not change volume', function(assert) {
|
||||
const player = TestHelpers.makePlayer({ techOrder: ['html5'] });
|
||||
const muteToggle = new MuteToggle(player);
|
||||
|
||||
assert.equal(player.volume(), 1, 'volume is above 0');
|
||||
assert.equal(player.muted(), false, 'player is not muted');
|
||||
|
||||
muteToggle.handleClick();
|
||||
|
||||
assert.equal(player.volume(), 1, 'volume is same');
|
||||
assert.equal(player.muted(), true, 'player is muted');
|
||||
});
|
||||
|
||||
QUnit.test('Clicking MuteToggle when volume is 0 and muted is false should set volume to lastVolume and keep muted false', function(assert) {
|
||||
const player = TestHelpers.makePlayer({ techOrder: ['html5'] });
|
||||
const muteToggle = new MuteToggle(player);
|
||||
|
||||
player.volume(0);
|
||||
assert.equal(player.lastVolume_(), 1, 'lastVolume is set');
|
||||
assert.equal(player.muted(), false, 'player is muted');
|
||||
|
||||
muteToggle.handleClick();
|
||||
|
||||
assert.equal(player.volume(), 1, 'volume is set to lastVolume');
|
||||
assert.equal(player.muted(), false, 'muted remains false');
|
||||
});
|
||||
|
||||
QUnit.test('Clicking MuteToggle when volume is 0 and muted is true should set volume to lastVolume and sets muted to false', function(assert) {
|
||||
const player = TestHelpers.makePlayer({ techOrder: ['html5'] });
|
||||
const muteToggle = new MuteToggle(player);
|
||||
|
||||
player.volume(0);
|
||||
player.muted(true);
|
||||
player.lastVolume_(0.5);
|
||||
|
||||
muteToggle.handleClick();
|
||||
|
||||
assert.equal(player.volume(), 0.5, 'volume is set to lastVolume');
|
||||
assert.equal(player.muted(), false, 'muted is set to false');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
/* eslint-env qunit */
|
||||
import sinon from 'sinon';
|
||||
import evented from '../../../src/js/mixins/evented';
|
||||
import * as Dom from '../../../src/js/utils/dom';
|
||||
import * as Obj from '../../../src/js/utils/obj';
|
||||
|
||||
// Common errors thrown by evented objects.
|
||||
const errors = {
|
||||
type: new Error('Invalid event type; must be a non-empty string or array.'),
|
||||
listener: new Error('Invalid listener; must be a function.'),
|
||||
target: new Error('Invalid target; must be a DOM node or evented object.')
|
||||
};
|
||||
|
||||
const validateListenerCall = (call, thisValue, eventExpectation) => {
|
||||
const eventActual = call.args[0];
|
||||
|
||||
QUnit.assert.strictEqual(call.thisValue, thisValue, 'the listener had the expected "this" value');
|
||||
QUnit.assert.strictEqual(typeof eventActual, 'object', 'the listener was passed an event object');
|
||||
|
||||
// We don't use `deepEqual` here because we only want to test a subset of
|
||||
// properties (designated by the `eventExpectation`).
|
||||
Object.keys(eventExpectation).forEach(key => {
|
||||
QUnit.assert.strictEqual(eventActual[key], eventExpectation[key], `the event had the expected "${key}"`);
|
||||
});
|
||||
};
|
||||
|
||||
QUnit.module('mixins: evented', {
|
||||
|
||||
beforeEach() {
|
||||
this.targets = {};
|
||||
},
|
||||
|
||||
afterEach() {
|
||||
Object.keys(this.targets).forEach(k => this.targets[k].trigger('dispose'));
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('evented() mutates an object as expected', function(assert) {
|
||||
const target = this.targets.a = {};
|
||||
|
||||
assert.strictEqual(typeof evented, 'function', 'the mixin is a function');
|
||||
assert.strictEqual(evented(target), target, 'returns the target object');
|
||||
|
||||
assert.ok(Obj.isObject(target), 'the target is still an object');
|
||||
assert.ok(Dom.isEl(target.eventBusEl_), 'the target has an event bus element');
|
||||
assert.strictEqual(typeof target.off, 'function', 'the target has an off method');
|
||||
assert.strictEqual(typeof target.on, 'function', 'the target has an on method');
|
||||
assert.strictEqual(typeof target.one, 'function', 'the target has a one method');
|
||||
assert.strictEqual(typeof target.trigger, 'function', 'the target has a trigger method');
|
||||
});
|
||||
|
||||
QUnit.test('evented() with custom element', function(assert) {
|
||||
const target = this.targets.a = evented({foo: Dom.createEl('span')}, {eventBusKey: 'foo'});
|
||||
|
||||
assert.strictEqual(target.eventBusEl_, target.foo, 'the custom DOM element is re-used');
|
||||
|
||||
assert.throws(
|
||||
() => evented({foo: {}}, {eventBusKey: 'foo'}),
|
||||
new Error('The eventBusKey "foo" does not refer to an element.'),
|
||||
'throws if the target does not have an element at the supplied key'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('on() and one() errors', function(assert) {
|
||||
const target = this.targets.a = evented({});
|
||||
|
||||
['on', 'one'].forEach(method => {
|
||||
assert.throws(() => target[method](), errors.type, 'the expected error is thrown');
|
||||
assert.throws(() => target[method](' '), errors.type, 'the expected error is thrown');
|
||||
assert.throws(() => target[method]([]), errors.type, 'the expected error is thrown');
|
||||
assert.throws(() => target[method]('x'), errors.listener, 'the expected error is thrown');
|
||||
assert.throws(() => target[method]({}, 'x', () => {}), errors.target, 'the expected error is thrown');
|
||||
assert.throws(() => target[method](evented({}), 'x', null), errors.listener, 'the expected error is thrown');
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('off() errors', function(assert) {
|
||||
const target = this.targets.a = evented({});
|
||||
|
||||
// An invalid event actually causes an invalid target error because it
|
||||
// gets passed into code that assumes the first argument is the target.
|
||||
assert.throws(() => target.off([]), errors.target, 'the expected error is thrown');
|
||||
assert.throws(() => target.off({}, 'x', () => {}), errors.target, 'the expected error is thrown');
|
||||
assert.throws(() => target.off(evented({}), '', () => {}), errors.type, 'the expected error is thrown');
|
||||
assert.throws(() => target.off(evented({}), [], () => {}), errors.type, 'the expected error is thrown');
|
||||
assert.throws(() => target.off(evented({}), 'x', null), errors.listener, 'the expected error is thrown');
|
||||
});
|
||||
|
||||
QUnit.test('on() can add a listener to one event type on this object', function(assert) {
|
||||
const a = this.targets.a = evented({});
|
||||
const spy = sinon.spy();
|
||||
|
||||
a.on('x', spy);
|
||||
a.trigger('x');
|
||||
|
||||
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spy.getCall(0), a, {
|
||||
type: 'x',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('on() can add a listener to an array of event types on this object', function(assert) {
|
||||
const a = this.targets.a = evented({});
|
||||
const spy = sinon.spy();
|
||||
|
||||
a.on(['x', 'y'], spy);
|
||||
a.trigger('x');
|
||||
a.trigger('y');
|
||||
|
||||
assert.strictEqual(spy.callCount, 2, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spy.getCall(0), a, {
|
||||
type: 'x',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
|
||||
validateListenerCall(spy.getCall(1), a, {
|
||||
type: 'y',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('one() can add a listener to one event type on this object', function(assert) {
|
||||
const a = this.targets.a = evented({});
|
||||
const spy = sinon.spy();
|
||||
|
||||
a.one('x', spy);
|
||||
a.trigger('x');
|
||||
a.trigger('x');
|
||||
|
||||
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spy.getCall(0), a, {
|
||||
type: 'x',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('one() can add a listener to an array of event types on this object', function(assert) {
|
||||
const a = this.targets.a = evented({});
|
||||
const spy = sinon.spy();
|
||||
|
||||
a.one(['x', 'y'], spy);
|
||||
a.trigger('x');
|
||||
a.trigger('y');
|
||||
a.trigger('x');
|
||||
a.trigger('y');
|
||||
|
||||
assert.strictEqual(spy.callCount, 2, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spy.getCall(0), a, {
|
||||
type: 'x',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
|
||||
validateListenerCall(spy.getCall(1), a, {
|
||||
type: 'y',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('on() can add a listener to one event type on a different target object', function(assert) {
|
||||
const a = this.targets.a = evented({});
|
||||
const b = this.targets.b = evented({});
|
||||
const spy = sinon.spy();
|
||||
|
||||
a.on(b, 'x', spy);
|
||||
b.trigger('x');
|
||||
|
||||
// Make sure we aren't magically binding a listener to "a".
|
||||
a.trigger('x');
|
||||
|
||||
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spy.getCall(0), a, {
|
||||
type: 'x',
|
||||
target: b.eventBusEl_
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('on() can add a listener to an array of event types on a different target object', function(assert) {
|
||||
const a = this.targets.a = evented({});
|
||||
const b = this.targets.b = evented({});
|
||||
const spy = sinon.spy();
|
||||
|
||||
a.on(b, ['x', 'y'], spy);
|
||||
b.trigger('x');
|
||||
b.trigger('y');
|
||||
|
||||
// Make sure we aren't magically binding a listener to "a".
|
||||
a.trigger('x');
|
||||
a.trigger('y');
|
||||
|
||||
assert.strictEqual(spy.callCount, 2, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spy.getCall(0), a, {
|
||||
type: 'x',
|
||||
target: b.eventBusEl_
|
||||
});
|
||||
|
||||
validateListenerCall(spy.getCall(1), a, {
|
||||
type: 'y',
|
||||
target: b.eventBusEl_
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('one() can add a listener to one event type on a different target object', function(assert) {
|
||||
const a = this.targets.a = evented({});
|
||||
const b = this.targets.b = evented({});
|
||||
const spy = sinon.spy();
|
||||
|
||||
a.one(b, 'x', spy);
|
||||
b.trigger('x');
|
||||
|
||||
// Make sure we aren't magically binding a listener to "a".
|
||||
a.trigger('x');
|
||||
|
||||
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spy.getCall(0), a, {
|
||||
type: 'x',
|
||||
target: b.eventBusEl_
|
||||
});
|
||||
});
|
||||
|
||||
// The behavior here unfortunately differs from the identical case where "a"
|
||||
// listens to itself. This is something that should be resolved...
|
||||
QUnit.test('one() can add a listener to an array of event types on a different target object', function(assert) {
|
||||
const a = this.targets.a = evented({});
|
||||
const b = this.targets.b = evented({});
|
||||
const spy = sinon.spy();
|
||||
|
||||
a.one(b, ['x', 'y'], spy);
|
||||
b.trigger('x');
|
||||
b.trigger('y');
|
||||
b.trigger('x');
|
||||
b.trigger('y');
|
||||
|
||||
// Make sure we aren't magically binding a listener to "a".
|
||||
a.trigger('x');
|
||||
a.trigger('y');
|
||||
|
||||
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spy.getCall(0), a, {
|
||||
type: 'x',
|
||||
target: b.eventBusEl_
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('off() with no arguments will remove all listeners from all events on this object', function(assert) {
|
||||
const a = this.targets.a = evented({});
|
||||
const spyX = sinon.spy();
|
||||
const spyY = sinon.spy();
|
||||
|
||||
a.on('x', spyX);
|
||||
a.on('y', spyY);
|
||||
a.trigger('x');
|
||||
a.trigger('y');
|
||||
a.off();
|
||||
a.trigger('x');
|
||||
a.trigger('y');
|
||||
|
||||
assert.strictEqual(spyX.callCount, 1, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spyX.getCall(0), a, {
|
||||
type: 'x',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
|
||||
assert.strictEqual(spyY.callCount, 1, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spyY.getCall(0), a, {
|
||||
type: 'y',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('off() can remove all listeners from a single event on this object', function(assert) {
|
||||
const a = this.targets.a = evented({});
|
||||
const spyX = sinon.spy();
|
||||
const spyY = sinon.spy();
|
||||
|
||||
a.on('x', spyX);
|
||||
a.on('y', spyY);
|
||||
a.trigger('x');
|
||||
a.trigger('y');
|
||||
a.off('x');
|
||||
a.trigger('x');
|
||||
a.trigger('y');
|
||||
|
||||
assert.strictEqual(spyX.callCount, 1, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spyX.getCall(0), a, {
|
||||
type: 'x',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
|
||||
assert.strictEqual(spyY.callCount, 2, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spyY.getCall(0), a, {
|
||||
type: 'y',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
|
||||
validateListenerCall(spyY.getCall(1), a, {
|
||||
type: 'y',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('off() can remove a listener from a single event on this object', function(assert) {
|
||||
const a = this.targets.a = evented({});
|
||||
const spyX1 = sinon.spy();
|
||||
const spyX2 = sinon.spy();
|
||||
const spyY = sinon.spy();
|
||||
|
||||
a.on('x', spyX1);
|
||||
a.on('x', spyX2);
|
||||
a.on('y', spyY);
|
||||
a.trigger('x');
|
||||
a.trigger('y');
|
||||
a.off('x', spyX1);
|
||||
a.trigger('x');
|
||||
a.trigger('y');
|
||||
|
||||
assert.strictEqual(spyX1.callCount, 1, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spyX1.getCall(0), a, {
|
||||
type: 'x',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
|
||||
assert.strictEqual(spyX2.callCount, 2, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spyX2.getCall(0), a, {
|
||||
type: 'x',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
|
||||
validateListenerCall(spyX2.getCall(1), a, {
|
||||
type: 'x',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
|
||||
assert.strictEqual(spyY.callCount, 2, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spyY.getCall(0), a, {
|
||||
type: 'y',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
|
||||
validateListenerCall(spyY.getCall(1), a, {
|
||||
type: 'y',
|
||||
target: a.eventBusEl_
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('off() can remove a listener from a single event on a different target object', function(assert) {
|
||||
const a = this.targets.a = evented({});
|
||||
const b = this.targets.b = evented({});
|
||||
const spy = sinon.spy();
|
||||
|
||||
a.on(b, 'x', spy);
|
||||
b.trigger('x');
|
||||
a.off(b, 'x', spy);
|
||||
b.trigger('x');
|
||||
|
||||
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spy.getCall(0), a, {
|
||||
type: 'x',
|
||||
target: b.eventBusEl_
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('off() can remove a listener from an array of events on a different target object', function(assert) {
|
||||
const a = this.targets.a = evented({});
|
||||
const b = this.targets.b = evented({});
|
||||
const spy = sinon.spy();
|
||||
|
||||
a.on(b, ['x', 'y'], spy);
|
||||
b.trigger('x');
|
||||
b.trigger('y');
|
||||
a.off(b, ['x', 'y'], spy);
|
||||
b.trigger('x');
|
||||
b.trigger('y');
|
||||
|
||||
assert.strictEqual(spy.callCount, 2, 'the listener was called the expected number of times');
|
||||
|
||||
validateListenerCall(spy.getCall(0), a, {
|
||||
type: 'x',
|
||||
target: b.eventBusEl_
|
||||
});
|
||||
|
||||
validateListenerCall(spy.getCall(1), a, {
|
||||
type: 'y',
|
||||
target: b.eventBusEl_
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
/* eslint-env qunit */
|
||||
import sinon from 'sinon';
|
||||
import evented from '../../../src/js/mixins/evented';
|
||||
import stateful from '../../../src/js/mixins/stateful';
|
||||
import * as Obj from '../../../src/js/utils/obj';
|
||||
|
||||
QUnit.module('mixins: stateful');
|
||||
|
||||
QUnit.test('stateful() mutates an object as expected', function(assert) {
|
||||
const target = {};
|
||||
|
||||
assert.strictEqual(Object.prototype.toString.call(stateful), '[object Function]', 'the mixin is a function');
|
||||
assert.strictEqual(stateful(target), target, 'returns the target object');
|
||||
|
||||
assert.ok(Obj.isObject(target), 'the target is still an object');
|
||||
assert.ok(Obj.isPlain(target.state), 'the target has a state');
|
||||
assert.strictEqual(Object.keys(target.state).length, 0, 'the target state is empty by default');
|
||||
assert.strictEqual(Object.prototype.toString.call(target.setState), '[object Function]', 'the target has a setState method');
|
||||
});
|
||||
|
||||
QUnit.test('stateful() with default state passed in', function(assert) {
|
||||
const target = stateful({}, {foo: 'bar'});
|
||||
|
||||
assert.strictEqual(target.state.foo, 'bar', 'the default properties are added to the state');
|
||||
});
|
||||
|
||||
QUnit.test('stateful() without default state passed in', function(assert) {
|
||||
const target = stateful({});
|
||||
|
||||
assert.strictEqual(Object.keys(target.state).length, 0, 'no default properties are added to the state');
|
||||
});
|
||||
|
||||
QUnit.test('setState() works as expected', function(assert) {
|
||||
const target = stateful(evented({}), {foo: 'bar', abc: 'xyz'});
|
||||
const spy = sinon.spy();
|
||||
|
||||
target.on('statechanged', spy);
|
||||
|
||||
const next = {foo: null, boo: 123};
|
||||
const changes = target.setState(next);
|
||||
|
||||
assert.deepEqual(changes, {
|
||||
foo: {from: 'bar', to: null},
|
||||
boo: {from: undefined, to: 123}
|
||||
}, 'setState returns changes, a plain object');
|
||||
|
||||
assert.deepEqual(target.state, {
|
||||
abc: 'xyz',
|
||||
foo: null,
|
||||
boo: 123
|
||||
}, 'the state was updated as expected');
|
||||
|
||||
assert.ok(spy.called, 'the "statechanged" event occurred');
|
||||
|
||||
const event = spy.firstCall.args[0];
|
||||
|
||||
assert.strictEqual(event.type, 'statechanged', 'the event had the expected type');
|
||||
assert.strictEqual(event.changes, changes, 'the changes object is sent along with the event');
|
||||
});
|
||||
|
||||
QUnit.test('setState() without changes does not trigger the "statechanged" event', function(assert) {
|
||||
const target = stateful(evented({}), {foo: 'bar'});
|
||||
const spy = sinon.spy();
|
||||
|
||||
target.on('statechanged', spy);
|
||||
|
||||
const changes = target.setState({foo: 'bar'});
|
||||
|
||||
assert.strictEqual(changes, undefined, 'no changes were returned');
|
||||
assert.strictEqual(spy.callCount, 0, 'no event was triggered');
|
||||
});
|
||||
|
||||
QUnit.test('handleStateChanged() is automatically bound to "statechanged" event', function(assert) {
|
||||
const target = evented({});
|
||||
|
||||
target.handleStateChanged = sinon.spy();
|
||||
stateful(target, {foo: 'bar'});
|
||||
|
||||
const changes = target.setState({foo: true});
|
||||
|
||||
assert.ok(target.handleStateChanged.called, 'the "statechanged" event occurred');
|
||||
|
||||
const event = target.handleStateChanged.firstCall.args[0];
|
||||
|
||||
assert.strictEqual(event.type, 'statechanged', 'the event had the expected type');
|
||||
assert.strictEqual(event.changes, changes, 'the handleStateChanged() method was called');
|
||||
});
|
||||
@@ -87,15 +87,6 @@ QUnit.test('should create a close button by default', function(assert) {
|
||||
assert.strictEqual(btn.el().parentNode, this.el, 'close button is a child of el');
|
||||
});
|
||||
|
||||
QUnit.test('returns `this` for expected methods', function(assert) {
|
||||
const methods = ['close', 'empty', 'fill', 'fillWith', 'open'];
|
||||
|
||||
assert.expect(methods.length);
|
||||
methods.forEach(function(method) {
|
||||
assert.strictEqual(this[method](), this, '`' + method + '()` returns `this`');
|
||||
}, this.modal);
|
||||
});
|
||||
|
||||
QUnit.test('open() triggers events', function(assert) {
|
||||
const modal = this.modal;
|
||||
const beforeModalOpenSpy = sinon.spy(function() {
|
||||
@@ -108,10 +99,9 @@ QUnit.test('open() triggers events', function(assert) {
|
||||
|
||||
assert.expect(4);
|
||||
|
||||
modal.
|
||||
on('beforemodalopen', beforeModalOpenSpy).
|
||||
on('modalopen', modalOpenSpy).
|
||||
open();
|
||||
modal.on('beforemodalopen', beforeModalOpenSpy);
|
||||
modal.on('modalopen', modalOpenSpy);
|
||||
modal.open();
|
||||
|
||||
assert.strictEqual(beforeModalOpenSpy.callCount, 1, 'beforemodalopen spy was called');
|
||||
assert.strictEqual(modalOpenSpy.callCount, 1, 'modalopen spy was called');
|
||||
@@ -127,7 +117,9 @@ QUnit.test('open() removes "vjs-hidden" class', function(assert) {
|
||||
QUnit.test('open() cannot be called on an opened modal', function(assert) {
|
||||
const spy = sinon.spy();
|
||||
|
||||
this.modal.on('modalopen', spy).open().open();
|
||||
this.modal.on('modalopen', spy);
|
||||
this.modal.open();
|
||||
this.modal.open();
|
||||
|
||||
assert.expect(1);
|
||||
assert.strictEqual(spy.callCount, 1, 'modal was only opened once');
|
||||
@@ -145,11 +137,10 @@ QUnit.test('close() triggers events', function(assert) {
|
||||
|
||||
assert.expect(4);
|
||||
|
||||
modal.
|
||||
on('beforemodalclose', beforeModalCloseSpy).
|
||||
on('modalclose', modalCloseSpy).
|
||||
open().
|
||||
close();
|
||||
modal.on('beforemodalclose', beforeModalCloseSpy);
|
||||
modal.on('modalclose', modalCloseSpy);
|
||||
modal.open();
|
||||
modal.close();
|
||||
|
||||
assert.strictEqual(beforeModalCloseSpy.callCount, 1, 'beforemodalclose spy was called');
|
||||
assert.strictEqual(modalCloseSpy.callCount, 1, 'modalclose spy was called');
|
||||
@@ -157,18 +148,21 @@ QUnit.test('close() triggers events', function(assert) {
|
||||
|
||||
QUnit.test('close() adds the "vjs-hidden" class', function(assert) {
|
||||
assert.expect(1);
|
||||
this.modal.open().close();
|
||||
this.modal.open();
|
||||
this.modal.close();
|
||||
assert.ok(this.modal.hasClass('vjs-hidden'), 'modal is hidden upon close');
|
||||
});
|
||||
|
||||
QUnit.test('pressing ESC triggers close(), but only when the modal is opened', function(assert) {
|
||||
const spy = sinon.spy();
|
||||
|
||||
this.modal.on('modalclose', spy).handleKeyPress({which: ESC});
|
||||
this.modal.on('modalclose', spy);
|
||||
this.modal.handleKeyPress({which: ESC});
|
||||
assert.expect(2);
|
||||
assert.strictEqual(spy.callCount, 0, 'ESC did not close the closed modal');
|
||||
|
||||
this.modal.open().handleKeyPress({which: ESC});
|
||||
this.modal.open();
|
||||
this.modal.handleKeyPress({which: ESC});
|
||||
assert.strictEqual(spy.callCount, 1, 'ESC closed the now-opened modal');
|
||||
});
|
||||
|
||||
@@ -176,7 +170,9 @@ QUnit.test('close() cannot be called on a closed modal', function(assert) {
|
||||
const spy = sinon.spy();
|
||||
|
||||
this.modal.on('modalclose', spy);
|
||||
this.modal.open().close().close();
|
||||
this.modal.open();
|
||||
this.modal.close();
|
||||
this.modal.close();
|
||||
|
||||
assert.expect(1);
|
||||
assert.strictEqual(spy.callCount, 1, 'modal was only closed once');
|
||||
@@ -227,11 +223,10 @@ QUnit.test('opened()', function(assert) {
|
||||
this.modal.open();
|
||||
assert.strictEqual(this.modal.opened(), true, 'the modal is open');
|
||||
|
||||
this.modal.
|
||||
close().
|
||||
on('modalopen', openSpy).
|
||||
on('modalclose', closeSpy).
|
||||
opened(true);
|
||||
this.modal.close();
|
||||
this.modal.on('modalopen', openSpy);
|
||||
this.modal.on('modalclose', closeSpy);
|
||||
this.modal.opened(true);
|
||||
|
||||
this.modal.opened(true);
|
||||
this.modal.opened(false);
|
||||
@@ -260,10 +255,9 @@ QUnit.test('fillWith()', function(assert) {
|
||||
contentEl.appendChild(el);
|
||||
});
|
||||
|
||||
this.modal.
|
||||
on('beforemodalfill', beforeFillSpy).
|
||||
on('modalfill', fillSpy).
|
||||
fillWith(children);
|
||||
this.modal.on('beforemodalfill', beforeFillSpy);
|
||||
this.modal.on('modalfill', fillSpy);
|
||||
this.modal.fillWith(children);
|
||||
|
||||
assert.expect(5 + children.length);
|
||||
assert.strictEqual(contentEl.children.length, children.length, 'has the right number of children');
|
||||
@@ -282,11 +276,10 @@ QUnit.test('empty()', function(assert) {
|
||||
const beforeEmptySpy = sinon.spy();
|
||||
const emptySpy = sinon.spy();
|
||||
|
||||
this.modal.
|
||||
fillWith([Dom.createEl(), Dom.createEl()]).
|
||||
on('beforemodalempty', beforeEmptySpy).
|
||||
on('modalempty', emptySpy).
|
||||
empty();
|
||||
this.modal.fillWith([Dom.createEl(), Dom.createEl()]);
|
||||
this.modal.on('beforemodalempty', beforeEmptySpy);
|
||||
this.modal.on('modalempty', emptySpy);
|
||||
this.modal.empty();
|
||||
|
||||
assert.expect(5);
|
||||
assert.strictEqual(this.modal.contentEl().children.length, 0, 'removed all `contentEl()` children');
|
||||
@@ -302,7 +295,8 @@ QUnit.test('closeable()', function(assert) {
|
||||
assert.expect(8);
|
||||
assert.strictEqual(this.modal.closeable(), true, 'the modal is closed');
|
||||
|
||||
this.modal.open().closeable(false);
|
||||
this.modal.open();
|
||||
this.modal.closeable(false);
|
||||
assert.notOk(this.modal.getChild('closeButton'), 'the close button is no longer a child of the modal');
|
||||
assert.notOk(initialCloseButton.el(), 'the initial close button was disposed');
|
||||
|
||||
@@ -312,13 +306,15 @@ QUnit.test('closeable()', function(assert) {
|
||||
this.modal.close();
|
||||
assert.notOk(this.modal.opened(), 'the modal was closed programmatically');
|
||||
|
||||
this.modal.open().closeable(true);
|
||||
this.modal.open();
|
||||
this.modal.closeable(true);
|
||||
assert.ok(this.modal.getChild('closeButton'), 'a new close button was created');
|
||||
|
||||
this.modal.getChild('closeButton').trigger('click');
|
||||
assert.notOk(this.modal.opened(), 'the modal was closed by the new close button');
|
||||
|
||||
this.modal.open().handleKeyPress({which: ESC});
|
||||
this.modal.open();
|
||||
this.modal.handleKeyPress({which: ESC});
|
||||
assert.notOk(this.modal.opened(), 'the modal was closed by the ESC key');
|
||||
});
|
||||
|
||||
@@ -331,7 +327,9 @@ QUnit.test('"content" option (fills on first open() invocation)', function(asser
|
||||
const spy = sinon.spy();
|
||||
|
||||
modal.on('modalfill', spy);
|
||||
modal.open().close().open();
|
||||
modal.open();
|
||||
modal.close();
|
||||
modal.open();
|
||||
|
||||
assert.expect(3);
|
||||
assert.strictEqual(modal.content(), modal.options_.content, 'has the expected content');
|
||||
@@ -347,8 +345,10 @@ QUnit.test('"temporary" option', function(assert) {
|
||||
|
||||
temp.on('dispose', tempSpy);
|
||||
perm.on('dispose', permSpy);
|
||||
temp.open().close();
|
||||
perm.open().close();
|
||||
temp.open();
|
||||
temp.close();
|
||||
perm.open();
|
||||
perm.close();
|
||||
|
||||
assert.expect(2);
|
||||
assert.strictEqual(tempSpy.callCount, 1, 'temporary modals are disposed');
|
||||
@@ -365,7 +365,9 @@ QUnit.test('"fillAlways" option', function(assert) {
|
||||
const spy = sinon.spy();
|
||||
|
||||
modal.on('modalfill', spy);
|
||||
modal.open().close().open();
|
||||
modal.open();
|
||||
modal.close();
|
||||
modal.open();
|
||||
|
||||
assert.expect(1);
|
||||
assert.strictEqual(spy.callCount, 2, 'the modal was filled on each open call');
|
||||
@@ -393,6 +395,7 @@ QUnit.test('"uncloseable" option', function(assert) {
|
||||
assert.strictEqual(modal.closeable(), false, 'the modal is uncloseable');
|
||||
assert.notOk(modal.getChild('closeButton'), 'the close button is not present');
|
||||
|
||||
modal.open().handleKeyPress({which: ESC});
|
||||
modal.open();
|
||||
modal.handleKeyPress({which: ESC});
|
||||
assert.strictEqual(spy.callCount, 0, 'ESC did not close the modal');
|
||||
});
|
||||
|
||||
+244
-58
@@ -1,4 +1,5 @@
|
||||
/* eslint-env qunit */
|
||||
import Plugin from '../../src/js/plugin';
|
||||
import Player from '../../src/js/player.js';
|
||||
import videojs from '../../src/js/video.js';
|
||||
import * as Dom from '../../src/js/utils/dom.js';
|
||||
@@ -6,12 +7,12 @@ import * as browser from '../../src/js/utils/browser.js';
|
||||
import log from '../../src/js/utils/log.js';
|
||||
import MediaError from '../../src/js/media-error.js';
|
||||
import Html5 from '../../src/js/tech/html5.js';
|
||||
import Tech from '../../src/js/tech/tech.js';
|
||||
import TestHelpers from './test-helpers.js';
|
||||
import document from 'global/document';
|
||||
import sinon from 'sinon';
|
||||
import window from 'global/window';
|
||||
import Tech from '../../src/js/tech/tech.js';
|
||||
import TechFaker from './tech/tech-faker.js';
|
||||
import * as middleware from '../../src/js/tech/middleware.js';
|
||||
|
||||
QUnit.module('Player', {
|
||||
beforeEach() {
|
||||
@@ -86,7 +87,7 @@ QUnit.test('should accept options from multiple sources and override in correct
|
||||
});
|
||||
|
||||
QUnit.test('should get tag, source, and track settings', function(assert) {
|
||||
// Partially tested in lib->getElAttributes
|
||||
// Partially tested in lib->getAttributes
|
||||
|
||||
const fixture = document.getElementById('qunit-fixture');
|
||||
|
||||
@@ -282,6 +283,9 @@ QUnit.test('should asynchronously fire error events during source selection', fu
|
||||
assert.ok(player.error().code === 4, 'Source could not be played error thrown');
|
||||
});
|
||||
|
||||
// The first one is for player initialization
|
||||
// The second one is the setTimeout for triggering the error
|
||||
this.clock.tick(1);
|
||||
this.clock.tick(1);
|
||||
|
||||
player.dispose();
|
||||
@@ -596,49 +600,6 @@ QUnit.test('make sure that controls listeners do not get added too many times',
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('should select the proper tech based on the the sourceOrder option', function(assert) {
|
||||
const fixture = document.getElementById('qunit-fixture');
|
||||
const html =
|
||||
'<video id="example_1">' +
|
||||
'<source src="fake.foo1" type="video/unsupported-format">' +
|
||||
'<source src="fake.foo2" type="video/foo-format">' +
|
||||
'</video>';
|
||||
|
||||
// Extend TechFaker to create a tech that plays the only mime-type that TechFaker
|
||||
// will not play
|
||||
class PlaysUnsupported extends TechFaker {
|
||||
constructor(options, handleReady) {
|
||||
super(options, handleReady);
|
||||
}
|
||||
// Support ONLY "video/unsupported-format"
|
||||
static isSupported() {
|
||||
return true;
|
||||
}
|
||||
static canPlayType(type) {
|
||||
return (type === 'video/unsupported-format' ? 'maybe' : '');
|
||||
}
|
||||
static canPlaySource(srcObj) {
|
||||
return srcObj.type === 'video/unsupported-format';
|
||||
}
|
||||
}
|
||||
Tech.registerTech('PlaysUnsupported', PlaysUnsupported);
|
||||
|
||||
fixture.innerHTML += html;
|
||||
let tag = document.getElementById('example_1');
|
||||
|
||||
let player = new Player(tag, { techOrder: ['techFaker', 'playsUnsupported'], sourceOrder: true });
|
||||
|
||||
assert.equal(player.techName_, 'PlaysUnsupported', 'selected the PlaysUnsupported tech when sourceOrder is truthy');
|
||||
player.dispose();
|
||||
|
||||
fixture.innerHTML += html;
|
||||
tag = document.getElementById('example_1');
|
||||
|
||||
player = new Player(tag, { techOrder: ['techFaker', 'playsUnsupported']});
|
||||
assert.equal(player.techName_, 'TechFaker', 'selected the TechFaker tech when sourceOrder is falsey');
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('should register players with generated ids', function(assert) {
|
||||
const fixture = document.getElementById('qunit-fixture');
|
||||
|
||||
@@ -973,11 +934,17 @@ QUnit.test('should clear pending errors on disposal', function(assert) {
|
||||
|
||||
const player = TestHelpers.makePlayer();
|
||||
|
||||
clock.tick(1);
|
||||
|
||||
player.src({
|
||||
src: 'http://example.com/movie.unsupported-format',
|
||||
type: 'video/unsupported-format'
|
||||
});
|
||||
|
||||
clock.tick(1);
|
||||
|
||||
player.dispose();
|
||||
|
||||
try {
|
||||
clock.tick(5000);
|
||||
} catch (e) {
|
||||
@@ -1066,6 +1033,48 @@ QUnit.test('should be scrubbing while seeking', function(assert) {
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
if (window.Promise) {
|
||||
QUnit.test('play promise should resolve to native promise if returned', function(assert) {
|
||||
const player = TestHelpers.makePlayer({});
|
||||
const done = assert.async();
|
||||
|
||||
player.src({
|
||||
src: 'http://example.com/video.mp4',
|
||||
type: 'video/mp4'
|
||||
});
|
||||
|
||||
this.clock.tick(1);
|
||||
|
||||
player.tech_.play = () => window.Promise.resolve('foo');
|
||||
const p = player.play();
|
||||
|
||||
assert.ok(p, 'play returns something');
|
||||
assert.equal(typeof p.then, 'function', 'play returns a promise');
|
||||
p.then(function(val) {
|
||||
assert.equal(val, 'foo', 'should resolve to native promise value');
|
||||
|
||||
player.dispose();
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.test('play promise should resolve to native value if returned', function(assert) {
|
||||
const player = TestHelpers.makePlayer({});
|
||||
|
||||
player.src({
|
||||
src: 'http://example.com/video.mp4',
|
||||
type: 'video/mp4'
|
||||
});
|
||||
|
||||
this.clock.tick(1);
|
||||
|
||||
player.tech_.play = () => 'foo';
|
||||
const p = player.play();
|
||||
|
||||
assert.equal(p, 'foo', 'play returns foo');
|
||||
});
|
||||
|
||||
QUnit.test('should throw on startup no techs are specified', function(assert) {
|
||||
const techOrder = videojs.options.techOrder;
|
||||
|
||||
@@ -1209,29 +1218,38 @@ QUnit.test('you can clear error in the error event', function(assert) {
|
||||
});
|
||||
|
||||
QUnit.test('Player#tech will return tech given the appropriate input', function(assert) {
|
||||
const oldLogWarn = log.warn;
|
||||
let warning;
|
||||
|
||||
log.warn = function(_warning) {
|
||||
warning = _warning;
|
||||
};
|
||||
|
||||
const tech_ = {};
|
||||
const returnedTech = Player.prototype.tech.call({tech_}, {IWillNotUseThisInPlugins: true});
|
||||
const returnedTech = Player.prototype.tech.call({tech_}, true);
|
||||
|
||||
assert.equal(returnedTech, tech_, 'We got back the tech we wanted');
|
||||
assert.notOk(warning, 'no warning was logged');
|
||||
|
||||
log.warn = oldLogWarn;
|
||||
});
|
||||
|
||||
QUnit.test('Player#tech alerts and throws without the appropriate input', function(assert) {
|
||||
let alertCalled;
|
||||
const oldAlert = window.alert;
|
||||
QUnit.test('Player#tech logs a warning when called without a safety argument', function(assert) {
|
||||
const oldLogWarn = log.warn;
|
||||
const warningRegex = new RegExp('https://github.com/videojs/video.js/issues/2617');
|
||||
let warning;
|
||||
|
||||
window.alert = () => {
|
||||
alertCalled = true;
|
||||
log.warn = function(_warning) {
|
||||
warning = _warning;
|
||||
};
|
||||
|
||||
const tech_ = {};
|
||||
|
||||
assert.throws(function() {
|
||||
Player.prototype.tech.call({tech_});
|
||||
}, new RegExp('https://github.com/videojs/video.js/issues/2617'),
|
||||
'we threw an error');
|
||||
Player.prototype.tech.call({tech_});
|
||||
|
||||
assert.ok(alertCalled, 'we called an alert');
|
||||
window.alert = oldAlert;
|
||||
assert.ok(warningRegex.test(warning), 'we logged a warning');
|
||||
|
||||
log.warn = oldLogWarn;
|
||||
});
|
||||
|
||||
QUnit.test('player#reset loads the Html5 tech and then techCalls reset', function(assert) {
|
||||
@@ -1387,3 +1405,171 @@ QUnit.test('should not allow to register custom player when any player has been
|
||||
// reset the Player to the original value;
|
||||
videojs.registerComponent('Player', Player);
|
||||
});
|
||||
|
||||
QUnit.test('techGet runs through middleware if allowedGetter', function(assert) {
|
||||
let cts = 0;
|
||||
let durs = 0;
|
||||
let ps = 0;
|
||||
|
||||
videojs.use('video/foo', () => ({
|
||||
currentTime() {
|
||||
cts++;
|
||||
},
|
||||
duration() {
|
||||
durs++;
|
||||
},
|
||||
paused() {
|
||||
ps++;
|
||||
}
|
||||
}));
|
||||
|
||||
const tag = TestHelpers.makeTag();
|
||||
const player = videojs(tag, {
|
||||
techOrder: ['techFaker']
|
||||
});
|
||||
|
||||
player.middleware_ = [middleware.getMiddleware('video/foo')[0](player)];
|
||||
|
||||
player.techGet_('currentTime');
|
||||
player.techGet_('duration');
|
||||
player.techGet_('paused');
|
||||
|
||||
assert.equal(cts, 1, 'currentTime is allowed');
|
||||
assert.equal(durs, 1, 'duration is allowed');
|
||||
assert.equal(ps, 0, 'paused is not allowed');
|
||||
|
||||
middleware.getMiddleware('video/foo').pop();
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('techCall runs through middleware if allowedSetter', function(assert) {
|
||||
let cts = 0;
|
||||
let vols = 0;
|
||||
|
||||
videojs.use('video/foo', () => ({
|
||||
setCurrentTime(ct) {
|
||||
cts++;
|
||||
return ct;
|
||||
},
|
||||
setVolume() {
|
||||
vols++;
|
||||
}
|
||||
}));
|
||||
|
||||
const tag = TestHelpers.makeTag();
|
||||
const player = videojs(tag, {
|
||||
techOrder: ['techFaker']
|
||||
});
|
||||
|
||||
player.middleware_ = [middleware.getMiddleware('video/foo')[0](player)];
|
||||
|
||||
this.clock.tick(1);
|
||||
|
||||
player.techCall_('setCurrentTime', 10);
|
||||
player.techCall_('setVolume', 0.5);
|
||||
|
||||
this.clock.tick(1);
|
||||
|
||||
assert.equal(cts, 1, 'setCurrentTime is allowed');
|
||||
assert.equal(vols, 0, 'setVolume is not allowed');
|
||||
|
||||
middleware.getMiddleware('video/foo').pop();
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('src selects tech based on middleware', function(assert) {
|
||||
class FooTech extends Html5 {}
|
||||
class BarTech extends Html5 {}
|
||||
|
||||
FooTech.isSupported = () => true;
|
||||
FooTech.canPlayType = (type) => type === 'video/mp4';
|
||||
FooTech.canPlaySource = (src) => FooTech.canPlayType(src.type);
|
||||
|
||||
BarTech.isSupported = () => true;
|
||||
BarTech.canPlayType = (type) => type === 'video/flv';
|
||||
BarTech.canPlaySource = (src) => BarTech.canPlayType(src.type);
|
||||
|
||||
videojs.registerTech('FooTech', FooTech);
|
||||
videojs.registerTech('BarTech', BarTech);
|
||||
|
||||
videojs.use('video/foo', () => ({
|
||||
setSource(src, next) {
|
||||
next(null, {
|
||||
src: 'http://example.com/video.mp4',
|
||||
type: 'video/mp4'
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
videojs.use('video/bar', () => ({
|
||||
setSource(src, next) {
|
||||
next(null, {
|
||||
src: 'http://example.com/video.flv',
|
||||
type: 'video/flv'
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
const tag = TestHelpers.makeTag();
|
||||
const player = videojs(tag, {
|
||||
techOrder: ['fooTech', 'barTech']
|
||||
});
|
||||
|
||||
player.src({
|
||||
src: 'foo',
|
||||
type: 'video/foo'
|
||||
});
|
||||
|
||||
this.clock.tick(1);
|
||||
|
||||
assert.equal(player.techName_, 'FooTech', 'the FooTech (html5) tech is chosen');
|
||||
|
||||
player.src({
|
||||
src: 'bar',
|
||||
type: 'video/bar'
|
||||
});
|
||||
|
||||
this.clock.tick(1);
|
||||
|
||||
assert.equal(player.techName_, 'BarTech', 'the BarTech (Flash) tech is chosen');
|
||||
|
||||
middleware.getMiddleware('video/foo').pop();
|
||||
middleware.getMiddleware('video/bar').pop();
|
||||
player.dispose();
|
||||
delete Tech.techs_.FooTech;
|
||||
delete Tech.techs_.BarTech;
|
||||
});
|
||||
|
||||
QUnit.test('options: plugins', function(assert) {
|
||||
const optionsSpy = sinon.spy();
|
||||
|
||||
Plugin.registerPlugin('foo', (options) => {
|
||||
optionsSpy(options);
|
||||
});
|
||||
|
||||
const player = TestHelpers.makePlayer({
|
||||
plugins: {
|
||||
foo: {
|
||||
bar: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(optionsSpy.callCount, 1, 'the plugin was set up');
|
||||
assert.deepEqual(optionsSpy.getCall(0).args[0], {bar: 1}, 'the plugin got the expected options');
|
||||
|
||||
assert.throws(
|
||||
() => {
|
||||
TestHelpers.makePlayer({
|
||||
plugins: {
|
||||
nope: {}
|
||||
}
|
||||
});
|
||||
},
|
||||
new Error('plugin "nope" does not exist'),
|
||||
'plugins that do not exist cause the player to throw'
|
||||
);
|
||||
|
||||
player.dispose();
|
||||
Plugin.deregisterPlugin('foo');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/* eslint-env qunit */
|
||||
import sinon from 'sinon';
|
||||
import Plugin from '../../src/js/plugin';
|
||||
import TestHelpers from './test-helpers';
|
||||
|
||||
QUnit.module('Plugin: advanced', {
|
||||
|
||||
beforeEach() {
|
||||
this.player = TestHelpers.makePlayer();
|
||||
const spy = this.spy = sinon.spy();
|
||||
|
||||
class MockPlugin extends Plugin {
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
spy.apply(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
this.MockPlugin = MockPlugin;
|
||||
Plugin.registerPlugin('mock', MockPlugin);
|
||||
},
|
||||
|
||||
afterEach() {
|
||||
this.player.dispose();
|
||||
|
||||
Object.keys(Plugin.getPlugins()).forEach(key => {
|
||||
if (key !== Plugin.BASE_PLUGIN_NAME) {
|
||||
Plugin.deregisterPlugin(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('pre-setup interface', function(assert) {
|
||||
assert.strictEqual(typeof this.player.plugin, 'undefined', 'the base Plugin does not add a method to the player');
|
||||
assert.strictEqual(typeof this.player.mock, 'function', 'plugins are a factory function on a player');
|
||||
assert.ok(this.player.hasPlugin('mock'), 'player has the plugin available');
|
||||
assert.strictEqual(this.player.mock.dispose, undefined, 'class-based plugins are not populated on a player until the factory method creates them');
|
||||
assert.notOk(this.player.usingPlugin('mock'), 'the player is not using the plugin');
|
||||
});
|
||||
|
||||
QUnit.test('setup', function(assert) {
|
||||
const instance = this.player.mock({foo: 'bar'}, 123);
|
||||
|
||||
assert.strictEqual(this.spy.callCount, 1, 'plugin was set up once');
|
||||
assert.strictEqual(this.spy.firstCall.thisValue, instance, 'plugin constructor `this` value was the instance');
|
||||
assert.deepEqual(this.spy.firstCall.args, [this.player, {foo: 'bar'}, 123], 'plugin had the correct arguments');
|
||||
assert.ok(this.player.usingPlugin('mock'), 'player now recognizes that the plugin was set up');
|
||||
assert.ok(this.player.hasPlugin('mock'), 'player has the plugin available');
|
||||
assert.ok(instance instanceof this.MockPlugin, 'plugin instance has the correct constructor');
|
||||
assert.strictEqual(instance, this.player.mock(), 'factory is replaced by method returning the instance');
|
||||
assert.strictEqual(instance.player, this.player, 'instance has a reference to the player');
|
||||
assert.strictEqual(instance.name, 'mock', 'instance knows its name');
|
||||
assert.strictEqual(typeof instance.state, 'object', 'instance is stateful');
|
||||
assert.strictEqual(typeof instance.setState, 'function', 'instance is stateful');
|
||||
assert.strictEqual(typeof instance.off, 'function', 'instance is evented');
|
||||
assert.strictEqual(typeof instance.on, 'function', 'instance is evented');
|
||||
assert.strictEqual(typeof instance.one, 'function', 'instance is evented');
|
||||
assert.strictEqual(typeof instance.trigger, 'function', 'instance is evented');
|
||||
assert.strictEqual(typeof instance.dispose, 'function', 'instance has dispose method');
|
||||
|
||||
assert.throws(
|
||||
() => new Plugin(this.player),
|
||||
new Error('Plugin must be sub-classed; not directly instantiated.'),
|
||||
'the Plugin class cannot be directly instantiated'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('"pluginsetup" event', function(assert) {
|
||||
const setupSpy = sinon.spy();
|
||||
|
||||
this.player.on('pluginsetup', setupSpy);
|
||||
|
||||
const instance = this.player.mock();
|
||||
const event = setupSpy.firstCall.args[0];
|
||||
const hash = setupSpy.firstCall.args[1];
|
||||
|
||||
assert.strictEqual(setupSpy.callCount, 1, 'the "pluginsetup" event was triggered');
|
||||
assert.strictEqual(event.type, 'pluginsetup', 'the event has the correct type');
|
||||
assert.strictEqual(event.target, this.player.el_, 'the event has the correct target');
|
||||
|
||||
assert.deepEqual(hash, {
|
||||
name: 'mock',
|
||||
instance,
|
||||
plugin: this.MockPlugin
|
||||
}, 'the event hash object is correct');
|
||||
});
|
||||
|
||||
QUnit.test('defaultState static property is used to populate state', function(assert) {
|
||||
class DefaultStateMock extends Plugin {}
|
||||
DefaultStateMock.defaultState = {foo: 1, bar: 2};
|
||||
Plugin.registerPlugin('dsm', DefaultStateMock);
|
||||
|
||||
const instance = this.player.dsm();
|
||||
|
||||
assert.deepEqual(instance.state, {foo: 1, bar: 2}, 'the plugin state has default properties');
|
||||
});
|
||||
|
||||
QUnit.test('dispose', function(assert) {
|
||||
const instance = this.player.mock();
|
||||
|
||||
instance.dispose();
|
||||
|
||||
assert.notOk(this.player.usingPlugin('mock'), 'player recognizes that the plugin is NOT set up');
|
||||
assert.ok(this.player.hasPlugin('mock'), 'player still has the plugin available');
|
||||
assert.strictEqual(typeof this.player.mock, 'function', 'instance is replaced by factory');
|
||||
assert.notStrictEqual(instance, this.player.mock, 'instance is replaced by factory');
|
||||
assert.strictEqual(instance.player, null, 'instance no longer has a reference to the player');
|
||||
assert.strictEqual(instance.state, null, 'state is now null');
|
||||
});
|
||||
|
||||
QUnit.test('"dispose" event', function(assert) {
|
||||
const disposeSpy = sinon.spy();
|
||||
const instance = this.player.mock();
|
||||
|
||||
instance.on('dispose', disposeSpy);
|
||||
instance.dispose();
|
||||
|
||||
assert.strictEqual(disposeSpy.callCount, 1, 'the "dispose" event was triggered');
|
||||
|
||||
const event = disposeSpy.firstCall.args[0];
|
||||
const hash = disposeSpy.firstCall.args[1];
|
||||
|
||||
assert.strictEqual(event.type, 'dispose', 'the event has the correct type');
|
||||
assert.strictEqual(event.target, instance.eventBusEl_, 'the event has the correct target');
|
||||
|
||||
assert.deepEqual(hash, {
|
||||
name: 'mock',
|
||||
instance,
|
||||
plugin: this.MockPlugin
|
||||
}, 'the event hash object is correct');
|
||||
});
|
||||
|
||||
QUnit.test('arbitrary events', function(assert) {
|
||||
const fooSpy = sinon.spy();
|
||||
const instance = this.player.mock();
|
||||
|
||||
instance.on('foo', fooSpy);
|
||||
instance.trigger('foo');
|
||||
|
||||
const event = fooSpy.firstCall.args[0];
|
||||
const hash = fooSpy.firstCall.args[1];
|
||||
|
||||
assert.strictEqual(fooSpy.callCount, 1, 'the "foo" event was triggered');
|
||||
assert.strictEqual(event.type, 'foo', 'the event has the correct type');
|
||||
assert.strictEqual(event.target, instance.eventBusEl_, 'the event has the correct target');
|
||||
|
||||
assert.deepEqual(hash, {
|
||||
name: 'mock',
|
||||
instance,
|
||||
plugin: this.MockPlugin
|
||||
}, 'the event hash object is correct');
|
||||
});
|
||||
|
||||
QUnit.test('handleStateChanged() method is automatically bound to the "statechanged" event', function(assert) {
|
||||
const spy = sinon.spy();
|
||||
|
||||
class TestHandler extends Plugin {}
|
||||
TestHandler.prototype.handleStateChanged = spy;
|
||||
Plugin.registerPlugin('testHandler', TestHandler);
|
||||
|
||||
const instance = this.player.testHandler();
|
||||
|
||||
instance.setState({foo: 1});
|
||||
assert.strictEqual(spy.callCount, 1, 'the handleStateChanged listener was called');
|
||||
assert.strictEqual(spy.firstCall.args[0].type, 'statechanged', 'the event was "statechanged"');
|
||||
assert.strictEqual(typeof spy.firstCall.args[0].changes, 'object', 'the event included a changes object');
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/* eslint-env qunit */
|
||||
import sinon from 'sinon';
|
||||
import Plugin from '../../src/js/plugin';
|
||||
import TestHelpers from './test-helpers';
|
||||
|
||||
QUnit.module('Plugin: basic', {
|
||||
|
||||
beforeEach() {
|
||||
this.basic = sinon.spy();
|
||||
this.player = TestHelpers.makePlayer();
|
||||
|
||||
Plugin.registerPlugin('basic', this.basic);
|
||||
},
|
||||
|
||||
afterEach() {
|
||||
this.player.dispose();
|
||||
|
||||
Object.keys(Plugin.getPlugins()).forEach(key => {
|
||||
if (key !== Plugin.BASE_PLUGIN_NAME) {
|
||||
Plugin.deregisterPlugin(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('pre-setup interface', function(assert) {
|
||||
assert.strictEqual(typeof this.player.basic, 'function', 'basic plugins are a function on a player');
|
||||
assert.ok(this.player.hasPlugin('basic'), 'player has the plugin available');
|
||||
assert.notStrictEqual(this.player.basic, this.basic, 'basic plugins are wrapped');
|
||||
assert.strictEqual(this.player.basic.dispose, undefined, 'unlike advanced plugins, basic plugins do not have a dispose method');
|
||||
assert.notOk(this.player.usingPlugin('basic'), 'the player is not using the plugin');
|
||||
});
|
||||
|
||||
QUnit.test('setup', function(assert) {
|
||||
this.player.basic({foo: 'bar'}, 123);
|
||||
assert.strictEqual(this.basic.callCount, 1, 'the plugin was called once');
|
||||
assert.strictEqual(this.basic.firstCall.thisValue, this.player, 'the plugin `this` value was the player');
|
||||
assert.deepEqual(this.basic.firstCall.args, [{foo: 'bar'}, 123], 'the plugin had the correct arguments');
|
||||
assert.ok(this.player.usingPlugin('basic'), 'the player now recognizes that the plugin was set up');
|
||||
assert.ok(this.player.hasPlugin('basic'), 'player has the plugin available');
|
||||
});
|
||||
|
||||
QUnit.test('"pluginsetup" event', function(assert) {
|
||||
const setupSpy = sinon.spy();
|
||||
|
||||
this.player.on('pluginsetup', setupSpy);
|
||||
|
||||
const instance = this.player.basic();
|
||||
const event = setupSpy.firstCall.args[0];
|
||||
const hash = setupSpy.firstCall.args[1];
|
||||
|
||||
assert.strictEqual(setupSpy.callCount, 1, 'the "pluginsetup" event was triggered');
|
||||
assert.strictEqual(event.type, 'pluginsetup', 'the event has the correct type');
|
||||
|
||||
assert.deepEqual(hash, {
|
||||
name: 'basic',
|
||||
instance,
|
||||
plugin: this.basic
|
||||
}, 'the event hash object is correct');
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
/* eslint-env qunit */
|
||||
import Player from '../../src/js/player';
|
||||
import Plugin from '../../src/js/plugin';
|
||||
|
||||
class MockPlugin extends Plugin {}
|
||||
|
||||
MockPlugin.VERSION = 'v1.2.3';
|
||||
|
||||
QUnit.module('Plugin: static methods', {
|
||||
|
||||
beforeEach() {
|
||||
this.basic = () => {};
|
||||
|
||||
Plugin.registerPlugin('basic', this.basic);
|
||||
Plugin.registerPlugin('mock', MockPlugin);
|
||||
},
|
||||
|
||||
afterEach() {
|
||||
Object.keys(Plugin.getPlugins()).forEach(key => {
|
||||
if (key !== Plugin.BASE_PLUGIN_NAME) {
|
||||
Plugin.deregisterPlugin(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('registerPlugin() works with basic plugins', function(assert) {
|
||||
const foo = () => {};
|
||||
|
||||
assert.strictEqual(Plugin.registerPlugin('foo', foo), foo, 'the plugin is returned');
|
||||
assert.strictEqual(Plugin.getPlugin('foo'), foo, 'the plugin can be retrieved');
|
||||
assert.strictEqual(Object.prototype.toString.call(Player.prototype.foo), '[object Function]', 'the plugin has a wrapper function');
|
||||
assert.notStrictEqual(Player.prototype.foo, foo, 'the function on the player prototype is a wrapper');
|
||||
|
||||
Plugin.deregisterPlugin('foo');
|
||||
});
|
||||
|
||||
QUnit.test('registerPlugin() works with class-based plugins', function(assert) {
|
||||
class Foo extends Plugin {}
|
||||
|
||||
assert.strictEqual(Plugin.registerPlugin('foo', Foo), Foo, 'the plugin is returned');
|
||||
assert.strictEqual(Plugin.getPlugin('foo'), Foo, 'the plugin can be retrieved');
|
||||
assert.strictEqual(Object.prototype.toString.call(Player.prototype.foo), '[object Function]', 'the plugin has a factory function');
|
||||
assert.notStrictEqual(Player.prototype.foo, Foo, 'the function on the player prototype is a factory');
|
||||
|
||||
Plugin.deregisterPlugin('foo');
|
||||
});
|
||||
|
||||
QUnit.test('registerPlugin() illegal arguments', function(assert) {
|
||||
assert.throws(
|
||||
() => Plugin.registerPlugin(),
|
||||
new Error('Illegal plugin name, "undefined", must be a string, was undefined.'),
|
||||
'plugins must have a name'
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => Plugin.registerPlugin('play'),
|
||||
new Error('Illegal plugin name, "play", already exists.'),
|
||||
'plugins cannot share a name with an existing player method'
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => Plugin.registerPlugin('foo'),
|
||||
new Error('Illegal plugin for "foo", must be a function, was undefined.'),
|
||||
'plugins require both arguments'
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => Plugin.registerPlugin('foo', {}),
|
||||
new Error('Illegal plugin for "foo", must be a function, was object.'),
|
||||
'plugins must be functions'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('getPlugin()', function(assert) {
|
||||
assert.ok(Plugin.getPlugin('basic'), 'the "basic" plugin exists');
|
||||
assert.ok(Plugin.getPlugin('mock'), 'the "mock" plugin exists');
|
||||
assert.strictEqual(Plugin.getPlugin(), undefined, 'returns undefined with no arguments');
|
||||
assert.strictEqual(Plugin.getPlugin('nonExistent'), undefined, 'returns undefined with non-existent plugin');
|
||||
assert.strictEqual(Plugin.getPlugin(123), undefined, 'returns undefined with an invalid type');
|
||||
});
|
||||
|
||||
QUnit.test('getPluginVersion()', function(assert) {
|
||||
assert.strictEqual(Plugin.getPluginVersion('basic'), '', 'the basic plugin has no version');
|
||||
assert.strictEqual(Plugin.getPluginVersion('mock'), 'v1.2.3', 'a plugin with a version returns its version');
|
||||
});
|
||||
|
||||
QUnit.test('getPlugins()', function(assert) {
|
||||
assert.strictEqual(Object.keys(Plugin.getPlugins()).length, 3, 'all plugins are returned by default');
|
||||
assert.strictEqual(Plugin.getPlugins().basic, this.basic, 'the "basic" plugin is included');
|
||||
assert.strictEqual(Plugin.getPlugins().mock, MockPlugin, 'the "mock" plugin is included');
|
||||
assert.strictEqual(Plugin.getPlugins().plugin, Plugin, 'the "plugin" plugin is included');
|
||||
assert.strictEqual(Object.keys(Plugin.getPlugins(['basic'])).length, 1, 'a subset of plugins can be requested');
|
||||
assert.strictEqual(Plugin.getPlugins(['basic']).basic, this.basic, 'the correct subset of plugins is returned');
|
||||
});
|
||||
|
||||
QUnit.test('deregisterPlugin()', function(assert) {
|
||||
const foo = () => {};
|
||||
|
||||
Plugin.registerPlugin('foo', foo);
|
||||
Plugin.deregisterPlugin('foo');
|
||||
|
||||
assert.strictEqual(Player.prototype.foo, undefined, 'the player prototype method is removed');
|
||||
assert.strictEqual(Plugin.getPlugin('foo'), undefined, 'the plugin can no longer be retrieved');
|
||||
|
||||
assert.throws(
|
||||
() => Plugin.deregisterPlugin('plugin'),
|
||||
new Error('Cannot de-register base plugin.'),
|
||||
'the base plugin cannot be de-registered'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('isBasic()', function(assert) {
|
||||
assert.ok(Plugin.isBasic(this.basic), 'the "basic" plugin is a basic plugin (by reference)');
|
||||
assert.ok(Plugin.isBasic('basic'), 'the "basic" plugin is a basic plugin (by name)');
|
||||
assert.notOk(Plugin.isBasic(MockPlugin), 'the "mock" plugin is NOT a basic plugin (by reference)');
|
||||
assert.notOk(Plugin.isBasic('mock'), 'the "mock" plugin is NOT a basic plugin (by name)');
|
||||
});
|
||||
@@ -1,215 +0,0 @@
|
||||
/* eslint-env qunit */
|
||||
import {IE_VERSION} from '../../src/js/utils/browser';
|
||||
import registerPlugin from '../../src/js/plugins.js';
|
||||
import Player from '../../src/js/player.js';
|
||||
import TestHelpers from './test-helpers.js';
|
||||
import window from 'global/window';
|
||||
import sinon from 'sinon';
|
||||
|
||||
QUnit.module('Plugins');
|
||||
|
||||
QUnit.test('Plugin should get initialized and receive options', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
registerPlugin('myPlugin1', function(options) {
|
||||
assert.ok(true, 'Plugin initialized');
|
||||
assert.ok(options.test, 'Option passed through');
|
||||
});
|
||||
|
||||
registerPlugin('myPlugin2', function(options) {
|
||||
assert.ok(false, 'Plugin initialized and should not have been');
|
||||
});
|
||||
|
||||
const player = TestHelpers.makePlayer({
|
||||
plugins: {
|
||||
myPlugin1: {
|
||||
test: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('Plugin should have the option of being initilized outside of player init', function(assert) {
|
||||
assert.expect(3);
|
||||
|
||||
registerPlugin('myPlugin3', function(options) {
|
||||
assert.ok(true, 'Plugin initialized after player init');
|
||||
assert.ok(options.test, 'Option passed through');
|
||||
});
|
||||
|
||||
const player = TestHelpers.makePlayer({});
|
||||
|
||||
assert.ok(player.myPlugin3, 'Plugin has direct access on player instance');
|
||||
|
||||
player.myPlugin3({
|
||||
test: true
|
||||
});
|
||||
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('Plugin should be able to add a UI component', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
registerPlugin('myPlugin4', function(options) {
|
||||
assert.ok((this instanceof Player), 'Plugin executed in player scope by default');
|
||||
this.addChild('component');
|
||||
});
|
||||
|
||||
const player = TestHelpers.makePlayer({});
|
||||
|
||||
player.myPlugin4({
|
||||
test: true
|
||||
});
|
||||
|
||||
const comp = player.getChild('component');
|
||||
|
||||
assert.ok(comp, 'Plugin added a component to the player');
|
||||
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('Plugin should overwrite plugin of same name', function(assert) {
|
||||
let v1Called = 0;
|
||||
let v2Called = 0;
|
||||
let v3Called = 0;
|
||||
|
||||
// Create initial plugin
|
||||
registerPlugin('myPlugin5', function(options) {
|
||||
v1Called++;
|
||||
});
|
||||
const player = TestHelpers.makePlayer({});
|
||||
|
||||
player.myPlugin5({});
|
||||
|
||||
// Overwrite and create new player
|
||||
registerPlugin('myPlugin5', function(options) {
|
||||
v2Called++;
|
||||
});
|
||||
const player2 = TestHelpers.makePlayer({});
|
||||
|
||||
player2.myPlugin5({});
|
||||
|
||||
// Overwrite and init new version on existing player
|
||||
registerPlugin('myPlugin5', function(options) {
|
||||
v3Called++;
|
||||
});
|
||||
player2.myPlugin5({});
|
||||
|
||||
assert.ok(v1Called === 1, 'First version of plugin called once');
|
||||
assert.ok(v2Called === 1, 'Plugin overwritten for new player');
|
||||
assert.ok(v3Called === 1, 'Plugin overwritten for existing player');
|
||||
|
||||
player.dispose();
|
||||
player2.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('Plugins should get events in registration order', function(assert) {
|
||||
const order = [];
|
||||
const expectedOrder = [];
|
||||
const pluginName = 'orderPlugin';
|
||||
const player = TestHelpers.makePlayer({});
|
||||
const plugin = function(name) {
|
||||
registerPlugin(name, function(opts) {
|
||||
this.on('test', function(event) {
|
||||
order.push(name);
|
||||
});
|
||||
});
|
||||
player[name]({});
|
||||
};
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const name = pluginName + i;
|
||||
|
||||
expectedOrder.push(name);
|
||||
plugin(name);
|
||||
}
|
||||
|
||||
registerPlugin('testerPlugin', function(opts) {
|
||||
this.trigger('test');
|
||||
});
|
||||
|
||||
player.testerPlugin({});
|
||||
|
||||
assert.deepEqual(order,
|
||||
expectedOrder,
|
||||
'plugins should receive events in order of initialization');
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('Plugins should not get events after stopImmediatePropagation is called', function(assert) {
|
||||
const order = [];
|
||||
const expectedOrder = [];
|
||||
const pluginName = 'orderPlugin';
|
||||
const player = TestHelpers.makePlayer({});
|
||||
const plugin = function(name) {
|
||||
registerPlugin(name, function(opts) {
|
||||
this.on('test', function(event) {
|
||||
order.push(name);
|
||||
event.stopImmediatePropagation();
|
||||
});
|
||||
});
|
||||
player[name]({});
|
||||
};
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const name = pluginName + i;
|
||||
|
||||
expectedOrder.push(name);
|
||||
plugin(name);
|
||||
}
|
||||
|
||||
registerPlugin('testerPlugin', function(opts) {
|
||||
this.trigger('test');
|
||||
});
|
||||
|
||||
player.testerPlugin({});
|
||||
|
||||
assert.deepEqual(order,
|
||||
expectedOrder.slice(0, order.length),
|
||||
'plugins should receive events in order of ' +
|
||||
'initialization, until stopImmediatePropagation');
|
||||
|
||||
assert.equal(order.length, 1, 'only one event listener should have triggered');
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('Plugin that does not exist logs an error', function(assert) {
|
||||
|
||||
const origConsole = window.console;
|
||||
|
||||
// stub the global log functions
|
||||
const console = window.console = {
|
||||
log() {},
|
||||
warn() {},
|
||||
error() {}
|
||||
};
|
||||
const log = sinon.stub(console, 'log');
|
||||
const error = sinon.stub(console, 'error');
|
||||
|
||||
// enable a non-existing plugin
|
||||
TestHelpers.makePlayer({
|
||||
plugins: {
|
||||
nonExistingPlugin: {
|
||||
foo: 'bar'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.ok(error.called, 'error was called');
|
||||
|
||||
if (IE_VERSION && IE_VERSION < 11) {
|
||||
assert.equal(error.firstCall.args[0],
|
||||
'VIDEOJS: ERROR: Unable to find plugin: nonExistingPlugin');
|
||||
} else {
|
||||
assert.equal(error.firstCall.args[2], 'Unable to find plugin:');
|
||||
assert.equal(error.firstCall.args[3], 'nonExistingPlugin');
|
||||
}
|
||||
|
||||
// tear down logging stubs
|
||||
log.restore();
|
||||
error.restore();
|
||||
window.console = origConsole;
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
/* eslint-env qunit */
|
||||
import Flash from '../../../src/js/tech/flash.js';
|
||||
|
||||
QUnit.module('Flash RTMP');
|
||||
|
||||
const streamToPartsAndBack = function(url) {
|
||||
const parts = Flash.streamToParts(url);
|
||||
|
||||
return Flash.streamFromParts(parts.connection, parts.stream);
|
||||
};
|
||||
|
||||
QUnit.test('test using both streamToParts and streamFromParts', function(assert) {
|
||||
assert.ok(streamToPartsAndBack('rtmp://myurl.com/isthis') === 'rtmp://myurl.com/&isthis');
|
||||
assert.ok(streamToPartsAndBack('rtmp://myurl.com/&isthis') === 'rtmp://myurl.com/&isthis');
|
||||
assert.ok(streamToPartsAndBack('rtmp://myurl.com/isthis/andthis') === 'rtmp://myurl.com/isthis/&andthis');
|
||||
});
|
||||
|
||||
QUnit.test('test streamToParts', function(assert) {
|
||||
let parts = Flash.streamToParts('http://myurl.com/streaming&/is/fun');
|
||||
|
||||
assert.ok(parts.connection === 'http://myurl.com/streaming');
|
||||
assert.ok(parts.stream === '/is/fun');
|
||||
|
||||
parts = Flash.streamToParts('http://myurl.com/&streaming&/is/fun');
|
||||
assert.ok(parts.connection === 'http://myurl.com/');
|
||||
assert.ok(parts.stream === 'streaming&/is/fun');
|
||||
|
||||
parts = Flash.streamToParts('http://myurl.com/really?streaming=fun&really=fun');
|
||||
assert.ok(parts.connection === 'http://myurl.com/');
|
||||
assert.ok(parts.stream === 'really?streaming=fun&really=fun');
|
||||
|
||||
parts = Flash.streamToParts('http://myurl.com/streaming/is/fun');
|
||||
assert.ok(parts.connection === 'http://myurl.com/streaming/is/');
|
||||
assert.ok(parts.stream === 'fun');
|
||||
|
||||
parts = Flash.streamToParts('whatisgoingonhere');
|
||||
assert.ok(parts.connection === 'whatisgoingonhere');
|
||||
assert.ok(parts.stream === '');
|
||||
|
||||
parts = Flash.streamToParts();
|
||||
assert.ok(parts.connection === '');
|
||||
assert.ok(parts.stream === '');
|
||||
});
|
||||
|
||||
QUnit.test('test isStreamingSrc', function(assert) {
|
||||
const isStreamingSrc = Flash.isStreamingSrc;
|
||||
|
||||
assert.ok(isStreamingSrc('rtmp://streaming.is/fun'));
|
||||
assert.ok(isStreamingSrc('rtmps://streaming.is/fun'));
|
||||
assert.ok(isStreamingSrc('rtmpe://streaming.is/fun'));
|
||||
assert.ok(isStreamingSrc('rtmpt://streaming.is/fun'));
|
||||
// test invalid protocols
|
||||
assert.ok(!isStreamingSrc('rtmp:streaming.is/fun'));
|
||||
assert.ok(!isStreamingSrc('rtmpz://streaming.is/fun'));
|
||||
assert.ok(!isStreamingSrc('http://streaming.is/fun'));
|
||||
assert.ok(!isStreamingSrc('https://streaming.is/fun'));
|
||||
assert.ok(!isStreamingSrc('file://streaming.is/fun'));
|
||||
});
|
||||
@@ -1,263 +0,0 @@
|
||||
/* eslint-env qunit */
|
||||
import Flash from '../../../src/js/tech/flash.js';
|
||||
import { createTimeRange } from '../../../src/js/utils/time-ranges.js';
|
||||
import document from 'global/document';
|
||||
import sinon from 'sinon';
|
||||
|
||||
// fake out the <object> interaction but leave all the other logic intact
|
||||
class MockFlash extends Flash {
|
||||
constructor() {
|
||||
super({});
|
||||
}
|
||||
}
|
||||
|
||||
QUnit.module('Flash');
|
||||
|
||||
QUnit.test('Flash.canPlaySource', function(assert) {
|
||||
const canPlaySource = Flash.canPlaySource;
|
||||
|
||||
// Supported
|
||||
assert.ok(canPlaySource({type: 'video/mp4; codecs=avc1.42E01E,mp4a.40.2' }, {}),
|
||||
'codecs supported');
|
||||
assert.ok(canPlaySource({type: 'video/mp4' }, {}), 'video/mp4 supported');
|
||||
assert.ok(canPlaySource({type: 'video/x-flv' }, {}), 'video/x-flv supported');
|
||||
assert.ok(canPlaySource({type: 'video/flv' }, {}), 'video/flv supported');
|
||||
assert.ok(canPlaySource({type: 'video/m4v' }, {}), 'video/m4v supported');
|
||||
assert.ok(canPlaySource({type: 'VIDEO/FLV' }, {}), 'capitalized mime type');
|
||||
|
||||
// Not supported
|
||||
assert.ok(!canPlaySource({ type: 'video/webm; codecs="vp8, vorbis"' }, {}));
|
||||
assert.ok(!canPlaySource({ type: 'video/webm' }, {}));
|
||||
});
|
||||
|
||||
QUnit.test('currentTime', function(assert) {
|
||||
const getCurrentTime = Flash.prototype.currentTime;
|
||||
const setCurrentTime = Flash.prototype.setCurrentTime;
|
||||
let seekingCount = 0;
|
||||
let seeking = false;
|
||||
let setPropVal;
|
||||
let getPropVal;
|
||||
let result;
|
||||
|
||||
// Mock out a Flash instance to avoid creating the swf object
|
||||
const mockFlash = {
|
||||
el_: {
|
||||
/* eslint-disable camelcase */
|
||||
vjs_setProperty(prop, val) {
|
||||
setPropVal = val;
|
||||
},
|
||||
vjs_getProperty() {
|
||||
return getPropVal;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
},
|
||||
seekable() {
|
||||
return createTimeRange(5, 1000);
|
||||
},
|
||||
trigger(event) {
|
||||
if (event === 'seeking') {
|
||||
seekingCount++;
|
||||
}
|
||||
},
|
||||
seeking() {
|
||||
return seeking;
|
||||
}
|
||||
};
|
||||
|
||||
// Test the currentTime getter
|
||||
getPropVal = 3;
|
||||
result = getCurrentTime.call(mockFlash);
|
||||
assert.equal(result, 3, 'currentTime is retreived from the swf element');
|
||||
|
||||
// Test the currentTime setter
|
||||
setCurrentTime.call(mockFlash, 10);
|
||||
assert.equal(setPropVal, 10, 'currentTime is set on the swf element');
|
||||
assert.equal(seekingCount, 1, 'triggered seeking');
|
||||
|
||||
// Test current time while seeking
|
||||
setCurrentTime.call(mockFlash, 20);
|
||||
seeking = true;
|
||||
result = getCurrentTime.call(mockFlash);
|
||||
assert.equal(result,
|
||||
20,
|
||||
'currentTime is retrieved from the lastSeekTarget while seeking');
|
||||
assert.notEqual(result,
|
||||
getPropVal,
|
||||
'currentTime is not retrieved from the element while seeking');
|
||||
assert.equal(seekingCount, 2, 'triggered seeking');
|
||||
|
||||
// clamp seeks to seekable
|
||||
setCurrentTime.call(mockFlash, 1001);
|
||||
result = getCurrentTime.call(mockFlash);
|
||||
assert.equal(result, mockFlash.seekable().end(0), 'clamped to the seekable end');
|
||||
assert.equal(seekingCount, 3, 'triggered seeking');
|
||||
|
||||
setCurrentTime.call(mockFlash, 1);
|
||||
result = getCurrentTime.call(mockFlash);
|
||||
assert.equal(result, mockFlash.seekable().start(0), 'clamped to the seekable start');
|
||||
assert.equal(seekingCount, 4, 'triggered seeking');
|
||||
});
|
||||
|
||||
QUnit.test('dispose removes the object element even before ready fires', function(assert) {
|
||||
// This test appears to test bad functionaly that was fixed
|
||||
// so it's debateable whether or not it's useful
|
||||
const dispose = Flash.prototype.dispose;
|
||||
const mockFlash = new MockFlash();
|
||||
const noop = function() {};
|
||||
|
||||
// Mock required functions for dispose
|
||||
mockFlash.off = noop;
|
||||
mockFlash.trigger = noop;
|
||||
mockFlash.el_ = {};
|
||||
|
||||
dispose.call(mockFlash);
|
||||
assert.strictEqual(mockFlash.el_, null, 'swf el is nulled');
|
||||
});
|
||||
|
||||
QUnit.test('ready triggering before and after disposing the tech', function(assert) {
|
||||
const checkReady = sinon.stub(Flash, 'checkReady');
|
||||
const fixtureDiv = document.getElementById('qunit-fixture');
|
||||
const playerDiv = document.createElement('div');
|
||||
const techEl = document.createElement('div');
|
||||
|
||||
techEl.id = 'foo1234';
|
||||
playerDiv.appendChild(techEl);
|
||||
fixtureDiv.appendChild(playerDiv);
|
||||
|
||||
// Mock the swf element
|
||||
techEl.tech = {
|
||||
el() {
|
||||
return techEl;
|
||||
}
|
||||
};
|
||||
|
||||
playerDiv.player = {
|
||||
tech: techEl.tech
|
||||
};
|
||||
|
||||
Flash.onReady(techEl.id);
|
||||
assert.ok(checkReady.called, 'checkReady should be called before the tech is disposed');
|
||||
|
||||
// remove the tech el from the player div to simulate being disposed
|
||||
playerDiv.removeChild(techEl);
|
||||
Flash.onReady(techEl.id);
|
||||
assert.ok(!checkReady.calledTwice,
|
||||
'checkReady should not be called after the tech is disposed');
|
||||
|
||||
Flash.checkReady.restore();
|
||||
});
|
||||
|
||||
QUnit.test('should have the source handler interface', function(assert) {
|
||||
assert.ok(Flash.registerSourceHandler, 'has the registerSourceHandler function');
|
||||
});
|
||||
|
||||
QUnit.test('canPlayType should select the correct types to play', function(assert) {
|
||||
const canPlayType = Flash.nativeSourceHandler.canPlayType;
|
||||
|
||||
assert.equal(canPlayType('video/flv'), 'maybe', 'should be able to play FLV files');
|
||||
assert.equal(canPlayType('video/x-flv'), 'maybe', 'should be able to play x-FLV files');
|
||||
assert.equal(canPlayType('video/mp4'), 'maybe', 'should be able to play MP4 files');
|
||||
assert.equal(canPlayType('video/m4v'), 'maybe', 'should be able to play M4V files');
|
||||
assert.equal(canPlayType('video/ogg'),
|
||||
'',
|
||||
'should return empty string if it can not play the video');
|
||||
});
|
||||
|
||||
QUnit.test('canHandleSource should be able to work with src objects without a type', function(assert) {
|
||||
const canHandleSource = Flash.nativeSourceHandler.canHandleSource;
|
||||
|
||||
assert.equal('maybe',
|
||||
canHandleSource({ src: 'test.video.mp4' }, {}),
|
||||
'should guess that it is a mp4 video');
|
||||
assert.equal('maybe',
|
||||
canHandleSource({ src: 'test.video.m4v' }, {}),
|
||||
'should guess that it is a m4v video');
|
||||
assert.equal('maybe',
|
||||
canHandleSource({ src: 'test.video.flv' }, {}),
|
||||
'should guess that it is a flash video');
|
||||
assert.equal('',
|
||||
canHandleSource({ src: 'test.video.wgg' }, {}),
|
||||
'should return empty string if it can not play the video');
|
||||
});
|
||||
|
||||
QUnit.test('seekable', function(assert) {
|
||||
const seekable = Flash.prototype.seekable;
|
||||
let result;
|
||||
const mockFlash = {
|
||||
duration() {
|
||||
return this.duration_;
|
||||
}
|
||||
};
|
||||
|
||||
// Test a normal duration
|
||||
mockFlash.duration_ = 23;
|
||||
result = seekable.call(mockFlash);
|
||||
assert.equal(result.length, 1, 'seekable is non-empty');
|
||||
assert.equal(result.start(0), 0, 'starts at zero');
|
||||
assert.equal(result.end(0), mockFlash.duration_, 'ends at the duration');
|
||||
|
||||
// Test a zero duration
|
||||
mockFlash.duration_ = 0;
|
||||
result = seekable.call(mockFlash);
|
||||
assert.equal(result.length, mockFlash.duration_,
|
||||
'seekable is empty with a zero duration');
|
||||
});
|
||||
|
||||
QUnit.test('play after ended seeks to the beginning', function(assert) {
|
||||
let plays = 0;
|
||||
const seeks = [];
|
||||
|
||||
Flash.prototype.play.call({
|
||||
el_: {
|
||||
/* eslint-disable camelcase */
|
||||
vjs_play() {
|
||||
plays++;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
},
|
||||
ended() {
|
||||
return true;
|
||||
},
|
||||
setCurrentTime(time) {
|
||||
seeks.push(time);
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(plays, 1, 'called play on the SWF');
|
||||
assert.equal(seeks.length, 1, 'seeked on play');
|
||||
assert.equal(seeks[0], 0, 'seeked to the beginning');
|
||||
});
|
||||
|
||||
QUnit.test('duration returns NaN, Infinity or duration according to the HTML standard', function(assert) {
|
||||
const duration = Flash.prototype.duration;
|
||||
let mockedDuration = -1;
|
||||
let mockedReadyState = 0;
|
||||
let result;
|
||||
const mockFlash = {
|
||||
el_: {
|
||||
/* eslint-disable camelcase */
|
||||
vjs_getProperty() {
|
||||
return mockedDuration;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
},
|
||||
readyState() {
|
||||
return mockedReadyState;
|
||||
}
|
||||
};
|
||||
|
||||
result = duration.call(mockFlash);
|
||||
assert.ok(Number.isNaN(result), 'duration returns NaN when readyState equals 0');
|
||||
|
||||
mockedReadyState = 1;
|
||||
result = duration.call(mockFlash);
|
||||
assert.ok(!Number.isFinite(result),
|
||||
'duration returns Infinity when duration property is less then 0');
|
||||
|
||||
mockedDuration = 1;
|
||||
result = duration.call(mockFlash);
|
||||
assert.equal(result,
|
||||
1,
|
||||
'duration returns duration property when readyState' +
|
||||
' and duration property are both higher than 0');
|
||||
});
|
||||
Alguns arquivos não foram exibidos porque demasiados arquivos foram alterados neste diff Mostrar Mais
Referência em uma Nova Issue
Bloquear um usuário