Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2fb922d73 | |||
| a6cc85c437 | |||
| d061be5d2b | |||
| dc2feedd79 | |||
| f8e4ba36e5 | |||
| f3d5389587 | |||
| d9ffb10b93 | |||
| 1186377b25 | |||
| 8616895e57 | |||
| 2cf5a22c85 | |||
| 763eb2df80 | |||
| 8bbf66a0fb | |||
| 676b46e4a7 | |||
| 82a119c67f | |||
| 6fd4389887 | |||
| 1e1a548459 | |||
| 8db9b53a8f | |||
| ba33fd8277 | |||
| 8071feda18 | |||
| a49b73cd01 | |||
| 8226493a9e | |||
| 38a8a0e8a1 | |||
| 6eeca8b5d1 | |||
| bf51ce4414 | |||
| 5ad614e251 | |||
| 93c890603d | |||
| 29fb4dfc2b | |||
| 7de9fd1d65 | |||
| 566c059832 | |||
| 0c9572f0a1 | |||
| c99607c85a | |||
| e2010bcd1a | |||
| e9f1b55f51 | |||
| 4f5152f526 | |||
| de9b53045a | |||
| df458c5e7a | |||
| e206554146 | |||
| e422806c44 | |||
| b6ddf144f4 | |||
| 86406ee59a | |||
| 81c5477f1d | |||
| ac64350a5f | |||
| 333619f1e3 | |||
| 17dcb63c26 | |||
| e04b90c9c0 | |||
| f62e1da01a | |||
| 2fe949629f | |||
| 20f2ddc11d | |||
| 004528a65c | |||
| 39c7bd40c2 | |||
| 43879e08f4 | |||
| 5ed7aa6620 | |||
| 47750b6aad | |||
| de7832eb8b | |||
| 52ea5bd0ab | |||
| 457d112df7 | |||
| 22cdec9d38 | |||
| d72e502107 | |||
| 94055f0772 | |||
| ede9323524 | |||
| c45f428f61 | |||
| b61ba02f3d | |||
| ea4d91d2a0 | |||
| 22d524ac9d | |||
| 8584f6a1db | |||
| 08df96a149 | |||
| cc3c0b5448 | |||
| 3c9c1b4cdc | |||
| 599883e684 | |||
| f1b4db4f36 | |||
| d4abb4b143 | |||
| 828ce66942 | |||
| ccc2608cf6 | |||
| de45de0e0b | |||
| 99c10aa1fc | |||
| 2a186e425b | |||
| 64bb206d85 | |||
| 2d6732d580 | |||
| 8f359adf9c | |||
| 1f09493ba2 | |||
| 115f352ade | |||
| 2af60c5c0d | |||
| aab2817ddc | |||
| f1c4752036 | |||
| 88735e3146 | |||
| c373ed72d7 | |||
| 213cfe8c84 | |||
| 87ea5e14b4 | |||
| 2aa967aba9 | |||
| d522e40594 | |||
| 3cd2b9a6c3 | |||
| 19e412a73a | |||
| cf5f77c709 | |||
| 4811e3333f | |||
| 8257857075 | |||
| e3e4e60fdb | |||
| 6ce9a94932 | |||
| fa5d0ad316 | |||
| 6bff6b317d | |||
| 99ac8d4c52 | |||
| 019e1f80ca | |||
| 2fe98f3721 | |||
| 5c08363400 | |||
| 927326f715 | |||
| 53933dff7e | |||
| f15c1344b0 | |||
| fb48b330cc | |||
| 5dddf8b0ec | |||
| 0ecf7e3854 | |||
| aae1092bac | |||
| 7158e507ad | |||
| 70f3390ffe | |||
| 392dfd024c | |||
| 87170ab460 | |||
| ee4c044d27 | |||
| 0b09b8ee6f | |||
| db95b3234f | |||
| 6d2dad5810 | |||
| 81ee3f759c | |||
| ed606c28ab | |||
| f15e07f7f5 | |||
| cd14c3086d | |||
| 41184b82ee | |||
| 6a6f3914c0 | |||
| 1bc452c349 | |||
| 3fad6ed42c | |||
| 62c263bda3 | |||
| 4c1337b4c5 | |||
| 38f10d4cc6 | |||
| 7c6d4666e9 | |||
| 90c5735904 |
+8
-15
@@ -5,8 +5,12 @@
|
|||||||
"browser": true,
|
"browser": true,
|
||||||
"es6": true
|
"es6": true
|
||||||
},
|
},
|
||||||
"globals": { "Plyr": false, "jQuery": false },
|
"globals": {
|
||||||
|
"Plyr": false,
|
||||||
|
"jQuery": false
|
||||||
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"import/no-cycle": 1,
|
||||||
"no-const-assign": 1,
|
"no-const-assign": 1,
|
||||||
"no-shadow": 0,
|
"no-shadow": 0,
|
||||||
"no-this-before-super": 1,
|
"no-this-before-super": 1,
|
||||||
@@ -21,20 +25,9 @@
|
|||||||
"eqeqeq": [2, "always"],
|
"eqeqeq": [2, "always"],
|
||||||
"one-var": [2, "never"],
|
"one-var": [2, "never"],
|
||||||
"comma-dangle": [2, "always-multiline"],
|
"comma-dangle": [2, "always-multiline"],
|
||||||
"no-restricted-globals": [
|
"spaced-comment": [2, "always"],
|
||||||
"error",
|
"no-restricted-globals": 2,
|
||||||
{
|
"no-param-reassign": [2, { "props": false }]
|
||||||
"name": "event",
|
|
||||||
"message": "Use local parameter instead."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "error",
|
|
||||||
"message": "Use local parameter instead."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-param-reassign": [2, { "props": false }],
|
|
||||||
"array-bracket-newline": [2, { "minItems": 2 }],
|
|
||||||
"array-element-newline": [2, { "minItems": 2 }]
|
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<!---
|
*Please _use this issue template_ as it makes replicating and fixing the issue easier for us. If you decide not to use it or you are vague your issue may be close instantly.*
|
||||||
Please use this issue template as it makes replicating and fixing the issue easier!
|
|
||||||
--->
|
|
||||||
|
|
||||||
### Expected behaviour
|
### Expected behaviour
|
||||||
|
|
||||||
@@ -14,4 +12,4 @@ Please use this issue template as it makes replicating and fixing the issue easi
|
|||||||
- Version:
|
- Version:
|
||||||
|
|
||||||
### Steps to reproduce
|
### Steps to reproduce
|
||||||
-
|
-
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
|
|
||||||
### Checklist
|
### Checklist
|
||||||
- [ ] Use `develop` as the base branch
|
- [ ] Use `develop` as the base branch
|
||||||
- [ ] Exclude the gulp build from the PR
|
- [ ] Exclude the gulp build (`/dist` changes) from the PR
|
||||||
- [ ] Test on [supported browsers](https://github.com/sampotts/plyr#browser-support)
|
- [ ] Test on [supported browsers](https://github.com/sampotts/plyr#browser-support)
|
||||||
|
|||||||
+3
-3
@@ -5,7 +5,7 @@ aws.json
|
|||||||
!dist/blank.mp4
|
!dist/blank.mp4
|
||||||
index-*.html
|
index-*.html
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
package-lock.json
|
||||||
*.webm
|
*.webm
|
||||||
/package-lock.json
|
.idea/
|
||||||
.idea/
|
|
||||||
|
|
||||||
@@ -2,3 +2,10 @@ demo
|
|||||||
.github
|
.github
|
||||||
.vscode
|
.vscode
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
|
aws.json
|
||||||
|
bundles.json
|
||||||
|
yarn.lock
|
||||||
|
package-lock.json
|
||||||
|
*.mp4
|
||||||
|
*.webm
|
||||||
|
!dist/blank.mp4
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"printWidth": 160,
|
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "all"
|
"trailingComma": "all"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
linters:
|
||||||
|
eslint:
|
||||||
|
files:
|
||||||
|
ignore:
|
||||||
|
- 'node_modules/*'
|
||||||
+5
-4
@@ -1,7 +1,8 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js: lts/*
|
||||||
- 'lts/*'
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- npm run lint
|
- bash .travis/prevent-base-master.sh
|
||||||
- npm run build
|
- bash .travis/omit-dist.sh
|
||||||
|
- npm run lint
|
||||||
|
- npm run build
|
||||||
|
|||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
if [ $TRAVIS_BRANCH == "develop" ] && $(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qE "^(demo/)?dist/"); then
|
||||||
|
echo 'Build output ("dist" and "demo/dist") not permitted in develop' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
if [ "$TRAVIS_PULL_REQUEST" != "false" ] && [ $TRAVIS_BRANCH == "master" ] && $(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -q "^src/"); then
|
||||||
|
echo 'The base branch for pull requests must be "develop"' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
+20
-11
@@ -2,20 +2,31 @@
|
|||||||
|
|
||||||
We welcome bug reports, feature requests and pull requests. If you want to help us out, please follow these guidelines, in order to avoid redundant work.
|
We welcome bug reports, feature requests and pull requests. If you want to help us out, please follow these guidelines, in order to avoid redundant work.
|
||||||
|
|
||||||
|
## Commenting
|
||||||
|
When commenting, keep a civil tone and stay on topic. Don't ask for support (use [Stack Overflow](https://stackoverflow.com/) or [our Slack](https://bit.ly/plyr-chat) for that), or post "+1" or "I agree" type of comments. Use the emojis instead.
|
||||||
|
|
||||||
|
Asking for the status on issues is discouraged. Unless someone has explicitly said in an issue that it's work in progress, most likely that means no one is working on it. We have a lot to do, and it may not be a top priority for us.
|
||||||
|
|
||||||
|
We *may* moderate discussions. We do this to avoid threads being "hijacked", to avoid confusion in case the content is misleading or outdated, and to avoid bothering people with github notifications.
|
||||||
|
|
||||||
## Reporting issues
|
## Reporting issues
|
||||||
|
|
||||||
Our GitHub issue tracker is for bug reports and feature requests. Don't create support issues here. Use [Stack Overflow](https://stackoverflow.com/) or [our Slack](https://bit.ly/plyr-chat) for that.
|
Our GitHub issue tracker is for bug reports and feature requests. Don't ask for support here. Use [Stack Overflow](https://stackoverflow.com/) or [our Slack](https://bit.ly/plyr-chat) for that.
|
||||||
|
|
||||||
Please verify that your issue hasn't already been answered by our FAQ (https://github.com/sampotts/plyr/wiki/FAQ), or that there isn't already an open issue for it.
|
Please verify that your issue hasn't already been answered by our [FAQ](https://github.com/sampotts/plyr/wiki/FAQ), and that there isn't already an open issue for it.
|
||||||
|
|
||||||
When applicable, check that your problem doesn't happen without Plyr (see [FAQ#1](https://github.com/sampotts/plyr/wiki/FAQ#1-does-plyr-work-with--)).
|
When applicable, check that your problem doesn't happen without Plyr (see [FAQ#1](https://github.com/sampotts/plyr/wiki/FAQ#1-does-plyr-work-with--)).
|
||||||
|
|
||||||
Verify that you are following the documentation, are using the latest version of Plyr, and aren't getting any errors in your own code, causing the issues.
|
Verify that you are following the documentation, are using the latest version of Plyr, and aren't getting any errors in your own code, causing the issues.
|
||||||
|
|
||||||
Describe the issue as detailed as possible, answering these questions:
|
Create one issue per problem or request (i.e. don't combine multiple problems to one git issue). Describe the issue as detailed as possible (see [Replication](#replication))
|
||||||
|
|
||||||
|
## Replication
|
||||||
|
|
||||||
|
In order to solve a problem, we first need to understand it. Please answer these questions when reporting issues or asking for help in [our Slack](https://bit.ly/plyr-chat).
|
||||||
|
|
||||||
* Does it happen only with specific options and/or specific browsers?
|
* Does it happen only with specific options and/or specific browsers?
|
||||||
* Does is happen only with HTML5 video, audio, youtube, vimeo or a specific library?
|
* Does is happen only with HTML5 video, audio, YouTube, Vimeo or a specific library?
|
||||||
* Does the issue happen on [our demo](https://plyr.io/)? If not, please recreate it with a **minimal** example online. You can use our Codepen templates to get started:
|
* Does the issue happen on [our demo](https://plyr.io/)? If not, please recreate it with a **minimal** example online. You can use our Codepen templates to get started:
|
||||||
* [HTML5 video](https://codepen.io/pen?template=bKeqpr)
|
* [HTML5 video](https://codepen.io/pen?template=bKeqpr)
|
||||||
* [HTML5 audio](https://codepen.io/pen?template=rKLywR)
|
* [HTML5 audio](https://codepen.io/pen?template=rKLywR)
|
||||||
@@ -25,15 +36,13 @@ Describe the issue as detailed as possible, answering these questions:
|
|||||||
* [Hls.js integration](https://codepen.io/pen?template=oyLKQb)
|
* [Hls.js integration](https://codepen.io/pen?template=oyLKQb)
|
||||||
* [Shaka Player integration](https://codepen.io/pen?template=ZRpzZO)
|
* [Shaka Player integration](https://codepen.io/pen?template=ZRpzZO)
|
||||||
|
|
||||||
It's important that you keep the issue description and replication demo **minimal**. If your implementation is using a framework, library or custom methods, which aren't needed to reproduce the issue, this makes it harder to debug and understand the issue. While it may be relevant to bring this up (ex: "I need Plyr to trigger the event sooner or it breaks Framework X") it also means that the person who is trying to fix the issue either has to know or learn your frameworks, libraries and custom methods, or that no one will try to fix your issue because it's too much work.
|
It's important that you keep the issue description and replication demo **minimal**. If your replication includes frameworks, libraries or customizations, this makes it harder to debug and understand the issue. While it may be relevant to bring this up (ex: "I need Plyr to trigger the event sooner or it breaks Framework X"), please keep these out of your replication demo if they aren't strictly needed to reproduce the issue. If the issue is caused by something a library does that Plyr doesn't handle, it's more helpful for us if you find out what it is, and replicate the same problem without the library. Otherwise any developer who is willing to help out with the issue has to understand the frameworks, libraries and customizations of *your* choice, or no one will try to fix your issue because it's too much work.
|
||||||
|
|
||||||
In order to keep things on topic and to avoid bothering people with github notifications, please don't combine multiple problems or bugs into one issue, don't comment on issues unless your comment is related to that issue, and don't post "+1" or "I agree" type of comments. Use the emojis instead.
|
|
||||||
|
|
||||||
Last but not least: Keep a civil tone in issues and comments. Non-constructive comments may be removed.
|
|
||||||
|
|
||||||
## Requesting features and improvements
|
## Requesting features and improvements
|
||||||
|
|
||||||
If you are missing something in Plyr, you can create a GitHub issue for this as well. Since we prioritize fixing bugs first, and may have a lot of other suggestions and architectural changes to work on as well, these may not be at the top of our list. If it's important or urgent to you, you may want to first ensure it's something we want to have in Plyr, and then contribute it as a pull request.
|
If you are missing something in Plyr, you can create a GitHub issue for this as well. Since we prioritize fixing bugs first, and may have a lot of other suggestions and architectural changes to work on as well, these may not be at the top of our list.
|
||||||
|
|
||||||
|
If your suggestion is important or urgent to you, you may want to first ensure it's something we want to have in Plyr, and then contribute it as a pull request. [Our Slack](https://bit.ly/plyr-chat) is the best place for questions like this.
|
||||||
|
|
||||||
## Contributing features and documentation
|
## Contributing features and documentation
|
||||||
|
|
||||||
@@ -43,7 +52,7 @@ If you are missing something in Plyr, you can create a GitHub issue for this as
|
|||||||
|
|
||||||
* Develop and test your modifications.
|
* Develop and test your modifications.
|
||||||
|
|
||||||
* Preferably commit your changes as independent logical chunks, with meaningful messages. Make sure you do not commit unnecessary files or changes, such as logging or breakpoints you added for testing, and the build output.
|
* Preferably commit your changes as independent logical chunks, with meaningful messages. Make sure you do not commit unnecessary files or changes, such as the build output, or logging and breakpoints you added for testing.
|
||||||
|
|
||||||
* If your modifications changes the documented behavior or add new features, document these changes in readme.md.
|
* If your modifications changes the documented behavior or add new features, document these changes in readme.md.
|
||||||
|
|
||||||
|
|||||||
-33
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "plyr",
|
|
||||||
"description": "A simple HTML5 media player using custom controls",
|
|
||||||
"homepage": "http://plyr.io",
|
|
||||||
"keywords": [
|
|
||||||
"Audio",
|
|
||||||
"Video",
|
|
||||||
"HTML5 Audio",
|
|
||||||
"HTML5 Video"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
"Sam Potts <sam@potts.es>"
|
|
||||||
],
|
|
||||||
"dependencies": {},
|
|
||||||
"main": [
|
|
||||||
"dist/plyr.css",
|
|
||||||
"dist/plyr.js",
|
|
||||||
"dist/plyr.svg",
|
|
||||||
"src/less/plyr.less",
|
|
||||||
"src/scss/plyr.scss",
|
|
||||||
"src/js/plyr.js"
|
|
||||||
],
|
|
||||||
"ignore": [
|
|
||||||
"node_modules",
|
|
||||||
"bower_components",
|
|
||||||
".gitignore"
|
|
||||||
],
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git://github.com/sampotts/plyr.git"
|
|
||||||
},
|
|
||||||
"license": "MIT"
|
|
||||||
}
|
|
||||||
+64
-1
@@ -1,3 +1,66 @@
|
|||||||
|
# v3.3.22
|
||||||
|
|
||||||
|
- Travis & CI improvements (thanks @friday)
|
||||||
|
- Add navigator.languages fallback for iOS 9 (thanks @friday)
|
||||||
|
|
||||||
|
# v3.3.21
|
||||||
|
|
||||||
|
- Hide currentTime and progress for streams (thanks @mimse)
|
||||||
|
- Fixed condition check (thanks @mimse)
|
||||||
|
- Handle undefined this.player.elements.buttons.play (thanks @klassicd)
|
||||||
|
- Fix captions.toggle() if there is no toggle button (thanks @friday)
|
||||||
|
|
||||||
|
# v3.3.20
|
||||||
|
|
||||||
|
- Fix for bug where controls wouldn't show on hover over YouTube video
|
||||||
|
|
||||||
|
# v3.3.19
|
||||||
|
|
||||||
|
- Remove `pointer-events: none` on embed `<iframe>` to comply with YouTube ToS
|
||||||
|
|
||||||
|
# 3.3.18
|
||||||
|
|
||||||
|
- Ads are now only supported on HTML5 videos as it violates terms of service for YouTube and Vimeo 😢
|
||||||
|
- Fix i18n defaults path on README (thanks @meyt!)
|
||||||
|
- Minor increaseVolume and decreaseVolume changes (thanks @friday!)
|
||||||
|
|
||||||
|
# v3.3.17
|
||||||
|
|
||||||
|
- Fix YouTube muting after seeking with the progress slider (thanks @friday!)
|
||||||
|
- Respect preload="none" when setting quality if the media hasn't been loaded some other way (thanks @friday!)
|
||||||
|
|
||||||
|
# v3.3.16
|
||||||
|
|
||||||
|
- Fixed regression relating the play button status (fixes #1048)
|
||||||
|
|
||||||
|
# v3.3.15
|
||||||
|
|
||||||
|
- Fix for error relating to play buttons when switching source
|
||||||
|
|
||||||
|
# v3.3.14
|
||||||
|
|
||||||
|
- Fix sprite loading regression
|
||||||
|
|
||||||
|
# v3.3.13
|
||||||
|
|
||||||
|
You guessed it, a load of awesome changes from contributors:
|
||||||
|
|
||||||
|
Thanks @friday for the following:
|
||||||
|
|
||||||
|
- Captions fixes
|
||||||
|
- Fix poster race conditions
|
||||||
|
- Minor code improvements for quality switching
|
||||||
|
- Minor event changes
|
||||||
|
- Fix condition in events.toggleListener to allow non-elements
|
||||||
|
- Suggestion: Remove array newline rule
|
||||||
|
- Contributions improvements
|
||||||
|
|
||||||
|
- fix: html5.cancelRequest not remove source tag correctly (thanks @a60814billy)
|
||||||
|
- remove event listeners in destroy() (thanks @cky917)
|
||||||
|
- Fix markdown in README (thanks @azu)
|
||||||
|
- Some parts of the accessibility improvements outlined in #905 (more on the way...)
|
||||||
|
- Fix for bug where volume slider didn't always show
|
||||||
|
|
||||||
# v3.3.12
|
# v3.3.12
|
||||||
|
|
||||||
- Fix synthetic event bubble/proxy loses detail (thanks @friday!)
|
- Fix synthetic event bubble/proxy loses detail (thanks @friday!)
|
||||||
@@ -21,7 +84,7 @@
|
|||||||
|
|
||||||
Again, more changes from @friday!
|
Again, more changes from @friday!
|
||||||
|
|
||||||
- Restore window reference in `utils.is.cue()`
|
- Restore window reference in `is.cue()`
|
||||||
- Fix InvalidStateError and IE11 issues
|
- Fix InvalidStateError and IE11 issues
|
||||||
- Respect storage being disabled for storage getter
|
- Respect storage being disabled for storage getter
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs. You can pass the following to the `controls` option:
|
This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs. You can pass the following to the `controls` option:
|
||||||
|
|
||||||
* `Array` of options (this builds the default controls based on your choices)
|
- `Array` of options (this builds the default controls based on your choices)
|
||||||
* `String` containing the desired HTML
|
- `String` containing the desired HTML
|
||||||
* `Function` that will be executed and should return one of the above
|
- `Function` that will be executed and should return one of the above
|
||||||
|
|
||||||
## Using default controls
|
## Using default controls
|
||||||
|
|
||||||
@@ -81,14 +81,14 @@ The classes and data attributes used in your template should match the `selector
|
|||||||
|
|
||||||
You need to add several placeholders to your HTML template that are replaced when rendering:
|
You need to add several placeholders to your HTML template that are replaced when rendering:
|
||||||
|
|
||||||
* `{id}` - the dynamically generated ID for the player (for form controls)
|
- `{id}` - the dynamically generated ID for the player (for form controls)
|
||||||
* `{seektime}` - the seek time specified in options for fast forward and rewind
|
- `{seektime}` - the seek time specified in options for fast forward and rewind
|
||||||
* `{title}` - the title of your media, if specified
|
- `{title}` - the title of your media, if specified
|
||||||
|
|
||||||
### Limitations
|
### Limitations
|
||||||
|
|
||||||
* Currently the settings menus are not supported with custom controls HTML
|
- Currently the settings menus are not supported with custom controls HTML
|
||||||
* AirPlay and PiP buttons can be added but you will have to manage feature detection
|
- AirPlay and PiP buttons can be added but you will have to manage feature detection
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ const controls = `
|
|||||||
<svg role="presentation"><use xlink:href="#plyr-rewind"></use></svg>
|
<svg role="presentation"><use xlink:href="#plyr-rewind"></use></svg>
|
||||||
<span class="plyr__tooltip" role="tooltip">Rewind {seektime} secs</span>
|
<span class="plyr__tooltip" role="tooltip">Rewind {seektime} secs</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Play, {title}" data-plyr="play">
|
<button type="button" class="plyr__control" aria-label="Play, {title}" data-plyr="play">
|
||||||
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-pause"></use></svg>
|
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-pause"></use></svg>
|
||||||
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-play"></use></svg>
|
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-play"></use></svg>
|
||||||
<span class="label--pressed plyr__tooltip" role="tooltip">Pause</span>
|
<span class="label--pressed plyr__tooltip" role="tooltip">Pause</span>
|
||||||
@@ -122,7 +122,7 @@ const controls = `
|
|||||||
</div>
|
</div>
|
||||||
<div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
|
<div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
|
||||||
<div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
|
<div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
|
||||||
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute">
|
<button type="button" class="plyr__control" aria-label="Mute" data-plyr="mute">
|
||||||
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
|
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
|
||||||
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
|
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
|
||||||
<span class="label--pressed plyr__tooltip" role="tooltip">Unmute</span>
|
<span class="label--pressed plyr__tooltip" role="tooltip">Unmute</span>
|
||||||
@@ -131,13 +131,13 @@ const controls = `
|
|||||||
<div class="plyr__volume">
|
<div class="plyr__volume">
|
||||||
<input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" aria-label="Volume">
|
<input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" aria-label="Volume">
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="plyr__control" aria-pressed="true" aria-label="Enable captions" data-plyr="captions">
|
<button type="button" class="plyr__control" data-plyr="captions">
|
||||||
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg>
|
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg>
|
||||||
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-captions-off"></use></svg>
|
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-captions-off"></use></svg>
|
||||||
<span class="label--pressed plyr__tooltip" role="tooltip">Disable captions</span>
|
<span class="label--pressed plyr__tooltip" role="tooltip">Disable captions</span>
|
||||||
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enable captions</span>
|
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enable captions</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Enter fullscreen" data-plyr="fullscreen">
|
<button type="button" class="plyr__control" data-plyr="fullscreen">
|
||||||
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-exit-fullscreen"></use></svg>
|
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-exit-fullscreen"></use></svg>
|
||||||
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-enter-fullscreen"></use></svg>
|
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-enter-fullscreen"></use></svg>
|
||||||
<span class="label--pressed plyr__tooltip" role="tooltip">Exit fullscreen</span>
|
<span class="label--pressed plyr__tooltip" role="tooltip">Exit fullscreen</span>
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+4392
-4367
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -96,7 +96,7 @@
|
|||||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576">
|
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576">
|
||||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720">
|
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720">
|
||||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
|
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
|
||||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4" type="video/mp4" size="1440">
|
<!-- <source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4" type="video/mp4" size="1440"> -->
|
||||||
|
|
||||||
<!-- Caption files -->
|
<!-- Caption files -->
|
||||||
<track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
|
<track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
|
||||||
|
|||||||
+18
-9
@@ -57,6 +57,7 @@ import Raven from 'raven-js';
|
|||||||
tooltips: {
|
tooltips: {
|
||||||
controls: true,
|
controls: true,
|
||||||
},
|
},
|
||||||
|
clickToPlay: false,
|
||||||
/* controls: [
|
/* controls: [
|
||||||
'play-large',
|
'play-large',
|
||||||
'restart',
|
'restart',
|
||||||
@@ -143,7 +144,11 @@ import Raven from 'raven-js';
|
|||||||
// Set a new source
|
// Set a new source
|
||||||
function newSource(type, init) {
|
function newSource(type, init) {
|
||||||
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
|
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
|
||||||
if (!(type in types) || (!init && type === currentType) || (!currentType.length && type === types.video)) {
|
if (
|
||||||
|
!(type in types) ||
|
||||||
|
(!init && type === currentType) ||
|
||||||
|
(!currentType.length && type === types.video)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,10 +220,12 @@ import Raven from 'raven-js';
|
|||||||
case types.youtube:
|
case types.youtube:
|
||||||
player.source = {
|
player.source = {
|
||||||
type: 'video',
|
type: 'video',
|
||||||
sources: [{
|
sources: [
|
||||||
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
|
{
|
||||||
provider: 'youtube',
|
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
|
||||||
}],
|
provider: 'youtube',
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -226,10 +233,12 @@ import Raven from 'raven-js';
|
|||||||
case types.vimeo:
|
case types.vimeo:
|
||||||
player.source = {
|
player.source = {
|
||||||
type: 'video',
|
type: 'video',
|
||||||
sources: [{
|
sources: [
|
||||||
src: 'https://vimeo.com/76979871',
|
{
|
||||||
provider: 'vimeo',
|
src: 'https://vimeo.com/76979871',
|
||||||
}],
|
provider: 'vimeo',
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+7618
-7664
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+13289
-13338
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+44
-32
@@ -1,33 +1,61 @@
|
|||||||
{
|
{
|
||||||
"name": "plyr",
|
"name": "plyr",
|
||||||
"version": "3.3.12",
|
"version": "3.3.22",
|
||||||
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
"description":
|
||||||
|
"A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
||||||
"homepage": "https://plyr.io",
|
"homepage": "https://plyr.io",
|
||||||
|
"author": "Sam Potts <sam@potts.es>",
|
||||||
|
"keywords": [
|
||||||
|
"HTML5 Video",
|
||||||
|
"HTML5 Audio",
|
||||||
|
"Media Player",
|
||||||
|
"DASH",
|
||||||
|
"Shaka",
|
||||||
|
"WordPress",
|
||||||
|
"HLS"
|
||||||
|
],
|
||||||
"main": "./dist/plyr.js",
|
"main": "./dist/plyr.js",
|
||||||
"browser": "./dist/plyr.min.js",
|
"browser": "./dist/plyr.min.js",
|
||||||
"sass": "./src/sass/plyr.scss",
|
"sass": "./src/sass/plyr.scss",
|
||||||
"style": "./dist/plyr.css",
|
"style": "./dist/plyr.css",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/sampotts/plyr.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/sampotts/plyr/issues"
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"doc": "readme.md"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "gulp build",
|
||||||
|
"lint": "eslint src/js && npm run-script remark",
|
||||||
|
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.26.3",
|
"babel-core": "^6.26.3",
|
||||||
"babel-eslint": "^8.2.3",
|
"babel-eslint": "^8.2.6",
|
||||||
"babel-plugin-external-helpers": "^6.22.0",
|
"babel-plugin-external-helpers": "^6.22.0",
|
||||||
"babel-preset-env": "^1.7.0",
|
"babel-preset-env": "^1.7.0",
|
||||||
"del": "^3.0.0",
|
"del": "^3.0.0",
|
||||||
"eslint": "^4.19.1",
|
"eslint": "^5.1.0",
|
||||||
"eslint-config-airbnb-base": "^12.1.0",
|
"eslint-config-airbnb-base": "^13.0.0",
|
||||||
"eslint-config-prettier": "^2.9.0",
|
"eslint-config-prettier": "^2.9.0",
|
||||||
"eslint-plugin-import": "^2.12.0",
|
"eslint-plugin-import": "^2.13.0",
|
||||||
"git-branch": "^2.0.1",
|
"git-branch": "^2.0.1",
|
||||||
"gulp": "^3.9.1",
|
"gulp": "^3.9.1",
|
||||||
"gulp-autoprefixer": "^5.0.0",
|
"gulp-autoprefixer": "^5.0.0",
|
||||||
"gulp-better-rollup": "^3.1.0",
|
"gulp-better-rollup": "^3.3.0",
|
||||||
"gulp-clean-css": "^3.9.4",
|
"gulp-clean-css": "^3.9.4",
|
||||||
"gulp-concat": "^2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
"gulp-filter": "^5.1.0",
|
"gulp-filter": "^5.1.0",
|
||||||
"gulp-header": "^2.0.5",
|
"gulp-header": "^2.0.5",
|
||||||
"gulp-open": "^3.0.1",
|
"gulp-open": "^3.0.1",
|
||||||
"gulp-postcss": "^7.0.1",
|
"gulp-postcss": "^7.0.1",
|
||||||
"gulp-rename": "^1.2.3",
|
"gulp-rename": "^1.3.0",
|
||||||
"gulp-replace": "^1.0.0",
|
"gulp-replace": "^1.0.0",
|
||||||
"gulp-s3": "^0.11.0",
|
"gulp-s3": "^0.11.0",
|
||||||
"gulp-sass": "^4.0.1",
|
"gulp-sass": "^4.0.1",
|
||||||
@@ -38,43 +66,27 @@
|
|||||||
"gulp-uglify-es": "^1.0.4",
|
"gulp-uglify-es": "^1.0.4",
|
||||||
"gulp-util": "^3.0.8",
|
"gulp-util": "^3.0.8",
|
||||||
"postcss-custom-properties": "^7.0.0",
|
"postcss-custom-properties": "^7.0.0",
|
||||||
"prettier-eslint": "^8.8.1",
|
"prettier-eslint": "^8.8.2",
|
||||||
"prettier-stylelint": "^0.4.2",
|
"prettier-stylelint": "^0.4.2",
|
||||||
"rollup-plugin-babel": "^3.0.4",
|
"remark-cli": "^5.0.0",
|
||||||
|
"remark-validate-links": "^7.0.0",
|
||||||
|
"rollup-plugin-babel": "^3.0.7",
|
||||||
"rollup-plugin-commonjs": "^9.1.3",
|
"rollup-plugin-commonjs": "^9.1.3",
|
||||||
"rollup-plugin-node-resolve": "^3.3.0",
|
"rollup-plugin-node-resolve": "^3.3.0",
|
||||||
"run-sequence": "^2.2.1",
|
"run-sequence": "^2.2.1",
|
||||||
"stylelint": "^9.2.1",
|
"stylelint": "^9.3.0",
|
||||||
"stylelint-config-prettier": "^3.2.0",
|
"stylelint-config-prettier": "^3.3.0",
|
||||||
"stylelint-config-recommended": "^2.1.0",
|
"stylelint-config-recommended": "^2.1.0",
|
||||||
"stylelint-config-sass-guidelines": "^5.0.0",
|
"stylelint-config-sass-guidelines": "^5.0.0",
|
||||||
"stylelint-order": "^0.8.1",
|
"stylelint-order": "^0.8.1",
|
||||||
"stylelint-scss": "^3.1.0",
|
"stylelint-scss": "^3.1.3",
|
||||||
"stylelint-selector-bem-pattern": "^2.0.0"
|
"stylelint-selector-bem-pattern": "^2.0.0"
|
||||||
},
|
},
|
||||||
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git://github.com/sampotts/plyr.git"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/sampotts/plyr/issues"
|
|
||||||
},
|
|
||||||
"directories": {
|
|
||||||
"doc": "readme.md"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "gulp build",
|
|
||||||
"lint": "eslint src/js",
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"author": "Sam Potts <sam@potts.es>",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"babel-polyfill": "^6.26.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"custom-event-polyfill": "^0.3.0",
|
"custom-event-polyfill": "^0.3.0",
|
||||||
"loadjs": "^3.5.4",
|
"loadjs": "^3.5.4",
|
||||||
"raven-js": "^3.26.1",
|
"raven-js": "^3.26.3",
|
||||||
"url-polyfill": "^1.0.13"
|
"url-polyfill": "^1.0.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -11,7 +11,8 @@
|
|||||||
},
|
},
|
||||||
// Exclude from search
|
// Exclude from search
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"dist/": true
|
"dist/": true,
|
||||||
|
"demo/dist/": true
|
||||||
},
|
},
|
||||||
// Linting
|
// Linting
|
||||||
"stylelint.enable": true,
|
"stylelint.enable": true,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ A simple, lightweight, accessible and customizable HTML5, YouTube and Vimeo medi
|
|||||||
* **HTML Video & Audio** - support for both formats
|
* **HTML Video & Audio** - support for both formats
|
||||||
* **[Embedded Video](#embeds)** - support for YouTube and Vimeo video playback
|
* **[Embedded Video](#embeds)** - support for YouTube and Vimeo video playback
|
||||||
* **[Monetization](#ads)** - make money from your videos
|
* **[Monetization](#ads)** - make money from your videos
|
||||||
* **[Streaming](#streaming)** - support for hls.js, Shaka and dash.js streaming playback
|
* **[Streaming](#try-plyr-online)** - support for hls.js, Shaka and dash.js streaming playback
|
||||||
* **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
|
* **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
|
||||||
* **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats
|
* **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats
|
||||||
* **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
|
* **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
|
||||||
@@ -132,13 +132,13 @@ See [initialising](#initialising) for more information on advanced setups.
|
|||||||
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
|
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="https://cdn.plyr.io/3.3.12/plyr.js"></script>
|
<script src="https://cdn.plyr.io/3.3.22/plyr.js"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
...or...
|
...or...
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="https://cdn.plyr.io/3.3.12/plyr.polyfilled.js"></script>
|
<script src="https://cdn.plyr.io/3.3.22/plyr.polyfilled.js"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
### CSS
|
### CSS
|
||||||
@@ -152,13 +152,13 @@ Include the `plyr.css` stylsheet into your `<head>`
|
|||||||
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
|
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.12/plyr.css">
|
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.22/plyr.css">
|
||||||
```
|
```
|
||||||
|
|
||||||
### SVG Sprite
|
### SVG Sprite
|
||||||
|
|
||||||
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
|
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
|
||||||
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.12/plyr.svg`.
|
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.22/plyr.svg`.
|
||||||
|
|
||||||
## Ads
|
## Ads
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ You can specify a range of arguments for the constructor to use:
|
|||||||
|
|
||||||
* A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
|
* A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
|
||||||
* A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
|
* A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
|
||||||
* A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
|
* A [`NodeList`](https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
|
||||||
* A [jQuery](https://jquery.com) object
|
* A [jQuery](https://jquery.com) object
|
||||||
|
|
||||||
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below.
|
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below.
|
||||||
@@ -286,7 +286,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
|||||||
| `debug` | Boolean | `false` | Display debugging information in the console |
|
| `debug` | Boolean | `false` | Display debugging information in the console |
|
||||||
| `controls` | Array, Function or Element | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function; `id` (the unique id for the player), `seektime` (the seektime step in seconds), and `title` (the media title). See [controls.md](controls.md) for more info on how the html needs to be structured. |
|
| `controls` | Array, Function or Element | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function; `id` (the unique id for the player), `seektime` (the seektime step in seconds), and `title` (the media title). See [controls.md](controls.md) for more info on how the html needs to be structured. |
|
||||||
| `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If you're using the default controls are used then you can specify which settings to show in the menu |
|
| `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If you're using the default controls are used then you can specify which settings to show in the menu |
|
||||||
| `i18n` | Object | See [defaults.js](/src/js/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
|
| `i18n` | Object | See [defaults.js](/src/js/config/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
|
||||||
| `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. |
|
| `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. |
|
||||||
| `iconUrl` | String | `null` | Specify a URL or path to the SVG sprite. See the [SVG section](#svg) for more info. |
|
| `iconUrl` | String | `null` | Specify a URL or path to the SVG sprite. See the [SVG section](#svg) for more info. |
|
||||||
| `iconPrefix` | String | `plyr` | Specify the id prefix for the icons used in the default controls (e.g. "plyr-play" would be "plyr"). This is to prevent clashes if you're using your own SVG sprite but with the default controls. Most people can ignore this option. |
|
| `iconPrefix` | String | `plyr` | Specify the id prefix for the icons used in the default controls (e.g. "plyr-play" would be "plyr"). This is to prevent clashes if you're using your own SVG sprite but with the default controls. Most people can ignore this option. |
|
||||||
@@ -367,6 +367,7 @@ player.fullscreen.enter(); // Enter fullscreen
|
|||||||
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
|
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
|
||||||
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
|
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
|
||||||
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
|
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
|
||||||
|
| `once(event, function)` | String, Function | Add an event listener for the specified event once. |
|
||||||
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
|
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
|
||||||
| `supports(type)` | String | Check support for a mime type. |
|
| `supports(type)` | String | Check support for a mime type. |
|
||||||
| `destroy()` | - | Destroy the instance and garbage collect any elements. |
|
| `destroy()` | - | Destroy the instance and garbage collect any elements. |
|
||||||
@@ -408,7 +409,7 @@ player.fullscreen.active; // false;
|
|||||||
| `speed` | ✓ | ✓ | Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5. |
|
| `speed` | ✓ | ✓ | Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5. |
|
||||||
| `quality`¹ | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
|
| `quality`¹ | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
|
||||||
| `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. |
|
| `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. |
|
||||||
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
|
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#the-source-setter) below for examples. |
|
||||||
| `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
|
| `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
|
||||||
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
|
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
|
||||||
| `currentTrack` | ✓ | ✓ | Gets or sets the caption track by index. `-1` means the track is missing or captions is not active |
|
| `currentTrack` | ✓ | ✓ | Gets or sets the caption track by index. `-1` means the track is missing or captions is not active |
|
||||||
@@ -434,10 +435,12 @@ player.source = {
|
|||||||
{
|
{
|
||||||
src: '/path/to/movie.mp4',
|
src: '/path/to/movie.mp4',
|
||||||
type: 'video/mp4',
|
type: 'video/mp4',
|
||||||
|
size: 720,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/path/to/movie.webm',
|
src: '/path/to/movie.webm',
|
||||||
type: 'video/webm',
|
type: 'video/webm',
|
||||||
|
size: 1080,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
poster: '/path/to/poster.jpg',
|
poster: '/path/to/poster.jpg',
|
||||||
|
|||||||
+181
-85
@@ -6,7 +6,21 @@
|
|||||||
import controls from './controls';
|
import controls from './controls';
|
||||||
import i18n from './i18n';
|
import i18n from './i18n';
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import utils from './utils';
|
import { dedupe } from './utils/arrays';
|
||||||
|
import browser from './utils/browser';
|
||||||
|
import {
|
||||||
|
createElement,
|
||||||
|
emptyElement,
|
||||||
|
getAttributesFromSelector,
|
||||||
|
insertAfter,
|
||||||
|
removeElement,
|
||||||
|
toggleClass,
|
||||||
|
} from './utils/elements';
|
||||||
|
import { on, triggerEvent } from './utils/events';
|
||||||
|
import fetch from './utils/fetch';
|
||||||
|
import is from './utils/is';
|
||||||
|
import { getHTML } from './utils/strings';
|
||||||
|
import { parseUrl } from './utils/urls';
|
||||||
|
|
||||||
const captions = {
|
const captions = {
|
||||||
// Setup captions
|
// Setup captions
|
||||||
@@ -19,7 +33,11 @@ const captions = {
|
|||||||
// Only Vimeo and HTML5 video supported at this point
|
// Only Vimeo and HTML5 video supported at this point
|
||||||
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
|
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
|
||||||
// Clear menu and hide
|
// Clear menu and hide
|
||||||
if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
|
if (
|
||||||
|
is.array(this.config.controls) &&
|
||||||
|
this.config.controls.includes('settings') &&
|
||||||
|
this.config.settings.includes('captions')
|
||||||
|
) {
|
||||||
controls.setCaptionsMenu.call(this);
|
controls.setCaptionsMenu.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,15 +45,12 @@ const captions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inject the container
|
// Inject the container
|
||||||
if (!utils.is.element(this.elements.captions)) {
|
if (!is.element(this.elements.captions)) {
|
||||||
this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions));
|
this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
|
||||||
|
|
||||||
utils.insertAfter(this.elements.captions, this.elements.wrapper);
|
insertAfter(this.elements.captions, this.elements.wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get browser info
|
|
||||||
const browser = utils.getBrowser();
|
|
||||||
|
|
||||||
// Fix IE captions if CORS is used
|
// Fix IE captions if CORS is used
|
||||||
// Fetch captions and inject as blobs instead (data URIs not supported!)
|
// Fetch captions and inject as blobs instead (data URIs not supported!)
|
||||||
if (browser.isIE && window.URL) {
|
if (browser.isIE && window.URL) {
|
||||||
@@ -43,84 +58,96 @@ const captions = {
|
|||||||
|
|
||||||
Array.from(elements).forEach(track => {
|
Array.from(elements).forEach(track => {
|
||||||
const src = track.getAttribute('src');
|
const src = track.getAttribute('src');
|
||||||
const href = utils.parseUrl(src);
|
const url = parseUrl(src);
|
||||||
|
|
||||||
if (href.hostname !== window.location.href.hostname && [
|
if (
|
||||||
'http:',
|
url !== null &&
|
||||||
'https:',
|
url.hostname !== window.location.href.hostname &&
|
||||||
].includes(href.protocol)) {
|
['http:', 'https:'].includes(url.protocol)
|
||||||
utils
|
) {
|
||||||
.fetch(src, 'blob')
|
fetch(src, 'blob')
|
||||||
.then(blob => {
|
.then(blob => {
|
||||||
track.setAttribute('src', window.URL.createObjectURL(blob));
|
track.setAttribute('src', window.URL.createObjectURL(blob));
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
utils.removeElement(track);
|
removeElement(track);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to load the value from storage
|
// Get and set initial data
|
||||||
let active = this.storage.get('captions');
|
// The "preferred" options are not realized unless / until the wanted language has a match
|
||||||
|
// * languages: Array of user's browser languages.
|
||||||
|
// * language: The language preferred by user settings or config
|
||||||
|
// * active: The state preferred by user settings or config
|
||||||
|
// * toggled: The real captions state
|
||||||
|
|
||||||
// Otherwise fall back to the default config
|
const languages = dedupe(
|
||||||
if (!utils.is.boolean(active)) {
|
Array.from(navigator.languages || navigator.language || navigator.userLanguage).map(language => language.split('-')[0]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
|
||||||
|
|
||||||
|
// Use first browser language when language is 'auto'
|
||||||
|
if (language === 'auto') {
|
||||||
|
[language] = languages;
|
||||||
|
}
|
||||||
|
|
||||||
|
let active = this.storage.get('captions');
|
||||||
|
if (!is.boolean(active)) {
|
||||||
({ active } = this.config.captions);
|
({ active } = this.config.captions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get language from storage, fallback to config
|
Object.assign(this.captions, {
|
||||||
let language = this.storage.get('language') || this.config.captions.language;
|
toggled: false,
|
||||||
if (language === 'auto') {
|
active,
|
||||||
[ language ] = (navigator.language || navigator.userLanguage).split('-');
|
language,
|
||||||
}
|
languages,
|
||||||
// Set language and show if active
|
});
|
||||||
captions.setLanguage.call(this, language, active);
|
|
||||||
|
|
||||||
// Watch changes to textTracks and update captions menu
|
// Watch changes to textTracks and update captions menu
|
||||||
if (this.isHTML5) {
|
if (this.isHTML5) {
|
||||||
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
|
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
|
||||||
utils.on(this.media.textTracks, trackEvents, captions.update.bind(this));
|
on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update available languages in list next tick (the event must not be triggered before the listeners)
|
// Update available languages in list next tick (the event must not be triggered before the listeners)
|
||||||
setTimeout(captions.update.bind(this), 0);
|
setTimeout(captions.update.bind(this), 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Update available language options in settings based on tracks
|
||||||
update() {
|
update() {
|
||||||
const tracks = captions.getTracks.call(this, true);
|
const tracks = captions.getTracks.call(this, true);
|
||||||
// Get the wanted language
|
// Get the wanted language
|
||||||
const { language, meta } = this.captions;
|
const { active, language, meta, currentTrackNode } = this.captions;
|
||||||
|
const languageExists = Boolean(tracks.find(track => track.language === language));
|
||||||
|
|
||||||
// Handle tracks (add event listener and "pseudo"-default)
|
// Handle tracks (add event listener and "pseudo"-default)
|
||||||
if (this.isHTML5 && this.isVideo) {
|
if (this.isHTML5 && this.isVideo) {
|
||||||
tracks
|
tracks.filter(track => !meta.get(track)).forEach(track => {
|
||||||
.filter(track => !meta.get(track))
|
this.debug.log('Track added', track);
|
||||||
.forEach(track => {
|
// Attempt to store if the original dom element was "default"
|
||||||
this.debug.log('Track added', track);
|
meta.set(track, {
|
||||||
// Attempt to store if the original dom element was "default"
|
default: track.mode === 'showing',
|
||||||
meta.set(track, {
|
|
||||||
default: track.mode === 'showing',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Turn off native caption rendering to avoid double captions
|
|
||||||
track.mode = 'hidden';
|
|
||||||
|
|
||||||
// Add event listener for cue changes
|
|
||||||
utils.on(track, 'cuechange', () => captions.updateCues.call(this));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Turn off native caption rendering to avoid double captions
|
||||||
|
track.mode = 'hidden';
|
||||||
|
|
||||||
|
// Add event listener for cue changes
|
||||||
|
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode);
|
// Update language first time it matches, or if the previous matching track was removed
|
||||||
const firstMatch = this.language !== language && tracks.find(track => track.language === language);
|
if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
|
||||||
|
captions.setLanguage.call(this, language);
|
||||||
// Update language if removed or first matching track added
|
captions.toggle.call(this, active && languageExists);
|
||||||
if (trackRemoved || firstMatch) {
|
|
||||||
captions.setLanguage.call(this, language, this.config.captions.active);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable or disable captions based on track length
|
// Enable or disable captions based on track length
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(tracks));
|
toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
|
||||||
|
|
||||||
// Update available languages in list
|
// Update available languages in list
|
||||||
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
|
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
|
||||||
@@ -128,16 +155,72 @@ const captions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
set(index, setLanguage = true, show = true) {
|
// Toggle captions display
|
||||||
|
// Used internally for the toggleCaptions method, with the passive option forced to false
|
||||||
|
toggle(input, passive = true) {
|
||||||
|
// If there's no full support
|
||||||
|
if (!this.supported.ui) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { toggled } = this.captions; // Current state
|
||||||
|
const activeClass = this.config.classNames.captions.active;
|
||||||
|
|
||||||
|
// Get the next state
|
||||||
|
// If the method is called without parameter, toggle based on current value
|
||||||
|
const active = is.nullOrUndefined(input) ? !toggled : input;
|
||||||
|
|
||||||
|
// Update state and trigger event
|
||||||
|
if (active !== toggled) {
|
||||||
|
// When passive, don't override user preferences
|
||||||
|
if (!passive) {
|
||||||
|
this.captions.active = active;
|
||||||
|
this.storage.set({ captions: active });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force language if the call isn't passive and there is no matching language to toggle to
|
||||||
|
if (!this.language && active && !passive) {
|
||||||
|
const tracks = captions.getTracks.call(this);
|
||||||
|
const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);
|
||||||
|
|
||||||
|
// Override user preferences to avoid switching languages if a matching track is added
|
||||||
|
this.captions.language = track.language;
|
||||||
|
|
||||||
|
// Set caption, but don't store in localStorage as user preference
|
||||||
|
captions.set.call(this, tracks.indexOf(track));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle button if it's enabled
|
||||||
|
if (this.elements.buttons.captions) {
|
||||||
|
this.elements.buttons.captions.pressed = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add class hook
|
||||||
|
toggleClass(this.elements.container, activeClass, active);
|
||||||
|
|
||||||
|
this.captions.toggled = active;
|
||||||
|
|
||||||
|
// Update settings menu
|
||||||
|
controls.updateSetting.call(this, 'captions');
|
||||||
|
|
||||||
|
// Trigger event (not used internally)
|
||||||
|
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Set captions by track index
|
||||||
|
// Used internally for the currentTrack setter with the passive option forced to false
|
||||||
|
set(index, passive = true) {
|
||||||
const tracks = captions.getTracks.call(this);
|
const tracks = captions.getTracks.call(this);
|
||||||
|
|
||||||
// Disable captions if setting to -1
|
// Disable captions if setting to -1
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
this.toggleCaptions(false);
|
captions.toggle.call(this, false, passive);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!utils.is.number(index)) {
|
if (!is.number(index)) {
|
||||||
this.debug.warn('Invalid caption argument', index);
|
this.debug.warn('Invalid caption argument', index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -149,15 +232,19 @@ const captions = {
|
|||||||
|
|
||||||
if (this.captions.currentTrack !== index) {
|
if (this.captions.currentTrack !== index) {
|
||||||
this.captions.currentTrack = index;
|
this.captions.currentTrack = index;
|
||||||
const track = captions.getCurrentTrack.call(this);
|
const track = tracks[index];
|
||||||
const { language } = track || {};
|
const { language } = track || {};
|
||||||
|
|
||||||
// Store reference to node for invalidation on remove
|
// Store reference to node for invalidation on remove
|
||||||
this.captions.currentTrackNode = track;
|
this.captions.currentTrackNode = track;
|
||||||
|
|
||||||
// Prevent setting language in some cases, since it can violate user's intentions
|
// Update settings menu
|
||||||
if (setLanguage) {
|
controls.updateSetting.call(this, 'captions');
|
||||||
|
|
||||||
|
// When passive, don't override user preferences
|
||||||
|
if (!passive) {
|
||||||
this.captions.language = language;
|
this.captions.language = language;
|
||||||
|
this.storage.set({ language });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Vimeo captions
|
// Handle Vimeo captions
|
||||||
@@ -166,32 +253,33 @@ const captions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trigger event
|
// Trigger event
|
||||||
utils.dispatchEvent.call(this, this.media, 'languagechange');
|
triggerEvent.call(this, this.media, 'languagechange');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show captions
|
||||||
|
captions.toggle.call(this, true, passive);
|
||||||
|
|
||||||
if (this.isHTML5 && this.isVideo) {
|
if (this.isHTML5 && this.isVideo) {
|
||||||
// If we change the active track while a cue is already displayed we need to update it
|
// If we change the active track while a cue is already displayed we need to update it
|
||||||
captions.updateCues.call(this);
|
captions.updateCues.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show captions
|
|
||||||
if (show) {
|
|
||||||
this.toggleCaptions(true);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setLanguage(language, show = true) {
|
// Set captions by language
|
||||||
if (!utils.is.string(language)) {
|
// Used internally for the language setter with the passive option forced to false
|
||||||
this.debug.warn('Invalid language argument', language);
|
setLanguage(input, passive = true) {
|
||||||
|
if (!is.string(input)) {
|
||||||
|
this.debug.warn('Invalid language argument', input);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Normalize
|
// Normalize
|
||||||
this.captions.language = language.toLowerCase();
|
const language = input.toLowerCase();
|
||||||
|
this.captions.language = language;
|
||||||
|
|
||||||
// Set currentTrack
|
// Set currentTrack
|
||||||
const tracks = captions.getTracks.call(this);
|
const tracks = captions.getTracks.call(this);
|
||||||
const track = captions.getCurrentTrack.call(this, true);
|
const track = captions.findTrack.call(this, [language]);
|
||||||
captions.set.call(this, tracks.indexOf(track), false, show);
|
captions.set.call(this, tracks.indexOf(track), passive);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get current valid caption tracks
|
// Get current valid caption tracks
|
||||||
@@ -204,34 +292,42 @@ const captions = {
|
|||||||
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
|
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
|
||||||
return tracks
|
return tracks
|
||||||
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
|
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
|
||||||
.filter(track => [
|
.filter(track => ['captions', 'subtitles'].includes(track.kind));
|
||||||
'captions',
|
|
||||||
'subtitles',
|
|
||||||
].includes(track.kind));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get the current track for the current language
|
// Match tracks based on languages and get the first
|
||||||
getCurrentTrack(fromLanguage = false) {
|
findTrack(languages, force = false) {
|
||||||
const tracks = captions.getTracks.call(this);
|
const tracks = captions.getTracks.call(this);
|
||||||
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
|
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
|
||||||
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
|
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
|
||||||
return (!fromLanguage && tracks[this.currentTrack]) || sorted.find(track => track.language === this.captions.language) || sorted[0];
|
let track;
|
||||||
|
languages.every(language => {
|
||||||
|
track = sorted.find(track => track.language === language);
|
||||||
|
return !track; // Break iteration if there is a match
|
||||||
|
});
|
||||||
|
// If no match is found but is required, get first
|
||||||
|
return track || (force ? sorted[0] : undefined);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get the current track
|
||||||
|
getCurrentTrack() {
|
||||||
|
return captions.getTracks.call(this)[this.currentTrack];
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get UI label for track
|
// Get UI label for track
|
||||||
getLabel(track) {
|
getLabel(track) {
|
||||||
let currentTrack = track;
|
let currentTrack = track;
|
||||||
|
|
||||||
if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) {
|
if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
|
||||||
currentTrack = captions.getCurrentTrack.call(this);
|
currentTrack = captions.getCurrentTrack.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (utils.is.track(currentTrack)) {
|
if (is.track(currentTrack)) {
|
||||||
if (!utils.is.empty(currentTrack.label)) {
|
if (!is.empty(currentTrack.label)) {
|
||||||
return currentTrack.label;
|
return currentTrack.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!utils.is.empty(currentTrack.language)) {
|
if (!is.empty(currentTrack.language)) {
|
||||||
return track.language.toUpperCase();
|
return track.language.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,13 +345,13 @@ const captions = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!utils.is.element(this.elements.captions)) {
|
if (!is.element(this.elements.captions)) {
|
||||||
this.debug.warn('No captions element to render to');
|
this.debug.warn('No captions element to render to');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only accept array or empty input
|
// Only accept array or empty input
|
||||||
if (!utils.is.nullOrUndefined(input) && !Array.isArray(input)) {
|
if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
|
||||||
this.debug.warn('updateCues: Invalid input', input);
|
this.debug.warn('updateCues: Invalid input', input);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -267,7 +363,7 @@ const captions = {
|
|||||||
const track = captions.getCurrentTrack.call(this);
|
const track = captions.getCurrentTrack.call(this);
|
||||||
cues = Array.from((track || {}).activeCues || [])
|
cues = Array.from((track || {}).activeCues || [])
|
||||||
.map(cue => cue.getCueAsHTML())
|
.map(cue => cue.getCueAsHTML())
|
||||||
.map(utils.getHTML);
|
.map(getHTML);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set new caption text
|
// Set new caption text
|
||||||
@@ -276,13 +372,13 @@ const captions = {
|
|||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
// Empty the container and create a new child element
|
// Empty the container and create a new child element
|
||||||
utils.emptyElement(this.elements.captions);
|
emptyElement(this.elements.captions);
|
||||||
const caption = utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.caption));
|
const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
|
||||||
caption.innerHTML = content;
|
caption.innerHTML = content;
|
||||||
this.elements.captions.appendChild(caption);
|
this.elements.captions.appendChild(caption);
|
||||||
|
|
||||||
// Trigger event
|
// Trigger event
|
||||||
utils.dispatchEvent.call(this, this.media, 'cuechange');
|
triggerEvent.call(this, this.media, 'cuechange');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const defaults = {
|
|||||||
// Only allow one media playing at once (vimeo only)
|
// Only allow one media playing at once (vimeo only)
|
||||||
autopause: true,
|
autopause: true,
|
||||||
|
|
||||||
|
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
|
||||||
|
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
|
||||||
|
playsinline: true,
|
||||||
|
|
||||||
// Default time to skip when rewind/fast forward
|
// Default time to skip when rewind/fast forward
|
||||||
seekTime: 10,
|
seekTime: 10,
|
||||||
|
|
||||||
@@ -89,15 +93,7 @@ const defaults = {
|
|||||||
// Speed default and options to display
|
// Speed default and options to display
|
||||||
speed: {
|
speed: {
|
||||||
selected: 1,
|
selected: 1,
|
||||||
options: [
|
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
|
||||||
0.5,
|
|
||||||
0.75,
|
|
||||||
1,
|
|
||||||
1.25,
|
|
||||||
1.5,
|
|
||||||
1.75,
|
|
||||||
2,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Keyboard shortcut settings
|
// Keyboard shortcut settings
|
||||||
@@ -151,11 +147,7 @@ const defaults = {
|
|||||||
'airplay',
|
'airplay',
|
||||||
'fullscreen',
|
'fullscreen',
|
||||||
],
|
],
|
||||||
settings: [
|
settings: ['captions', 'quality', 'speed'],
|
||||||
'captions',
|
|
||||||
'quality',
|
|
||||||
'speed',
|
|
||||||
],
|
|
||||||
|
|
||||||
// Localisation
|
// Localisation
|
||||||
i18n: {
|
i18n: {
|
||||||
@@ -165,6 +157,7 @@ const defaults = {
|
|||||||
pause: 'Pause',
|
pause: 'Pause',
|
||||||
fastForward: 'Forward {seektime}s',
|
fastForward: 'Forward {seektime}s',
|
||||||
seek: 'Seek',
|
seek: 'Seek',
|
||||||
|
seekLabel: '{currentTime} of {duration}',
|
||||||
played: 'Played',
|
played: 'Played',
|
||||||
buffered: 'Buffered',
|
buffered: 'Buffered',
|
||||||
currentTime: 'Current time',
|
currentTime: 'Current time',
|
||||||
@@ -179,6 +172,7 @@ const defaults = {
|
|||||||
frameTitle: 'Player for {title}',
|
frameTitle: 'Player for {title}',
|
||||||
captions: 'Captions',
|
captions: 'Captions',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
|
menuBack: 'Go back to previous menu',
|
||||||
speed: 'Speed',
|
speed: 'Speed',
|
||||||
normal: 'Normal',
|
normal: 'Normal',
|
||||||
quality: 'Quality',
|
quality: 'Quality',
|
||||||
@@ -209,7 +203,8 @@ const defaults = {
|
|||||||
},
|
},
|
||||||
youtube: {
|
youtube: {
|
||||||
sdk: 'https://www.youtube.com/iframe_api',
|
sdk: 'https://www.youtube.com/iframe_api',
|
||||||
api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
|
api:
|
||||||
|
'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
|
||||||
},
|
},
|
||||||
googleIMA: {
|
googleIMA: {
|
||||||
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
||||||
@@ -345,6 +340,7 @@ const defaults = {
|
|||||||
posterEnabled: 'plyr__poster-enabled',
|
posterEnabled: 'plyr__poster-enabled',
|
||||||
ads: 'plyr__ads',
|
ads: 'plyr__ads',
|
||||||
control: 'plyr__control',
|
control: 'plyr__control',
|
||||||
|
controlPressed: 'plyr__control--pressed',
|
||||||
playing: 'plyr--playing',
|
playing: 'plyr--playing',
|
||||||
paused: 'plyr--paused',
|
paused: 'plyr--paused',
|
||||||
stopped: 'plyr--stopped',
|
stopped: 'plyr--stopped',
|
||||||
@@ -13,4 +13,22 @@ export const types = {
|
|||||||
video: 'video',
|
video: 'video',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get provider by URL
|
||||||
|
* @param {string} url
|
||||||
|
*/
|
||||||
|
export function getProviderByUrl(url) {
|
||||||
|
// YouTube
|
||||||
|
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) {
|
||||||
|
return providers.youtube;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vimeo
|
||||||
|
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
|
||||||
|
return providers.vimeo;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export default { providers, types };
|
export default { providers, types };
|
||||||
@@ -17,10 +17,12 @@ export default class Console {
|
|||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
|
return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
|
||||||
}
|
}
|
||||||
|
|
||||||
get warn() {
|
get warn() {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
|
return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
|
||||||
}
|
}
|
||||||
|
|
||||||
get error() {
|
get error() {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
|
return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
|
||||||
|
|||||||
Vendored
+299
-288
File diff suppressed because it is too large
Load Diff
+36
-27
@@ -3,9 +3,10 @@
|
|||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing
|
// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import utils from './utils';
|
import browser from './utils/browser';
|
||||||
|
import { hasClass, toggleClass, trapFocus } from './utils/elements';
|
||||||
const browser = utils.getBrowser();
|
import { on, triggerEvent } from './utils/events';
|
||||||
|
import is from './utils/is';
|
||||||
|
|
||||||
function onChange() {
|
function onChange() {
|
||||||
if (!this.enabled) {
|
if (!this.enabled) {
|
||||||
@@ -14,16 +15,16 @@ function onChange() {
|
|||||||
|
|
||||||
// Update toggle button
|
// Update toggle button
|
||||||
const button = this.player.elements.buttons.fullscreen;
|
const button = this.player.elements.buttons.fullscreen;
|
||||||
if (utils.is.element(button)) {
|
if (is.element(button)) {
|
||||||
utils.toggleState(button, this.active);
|
button.pressed = this.active;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger an event
|
// Trigger an event
|
||||||
utils.dispatchEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
|
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
|
||||||
|
|
||||||
// Trap focus in container
|
// Trap focus in container
|
||||||
if (!browser.isIos) {
|
if (!browser.isIos) {
|
||||||
utils.trapFocus.call(this.player, this.target, this.active);
|
trapFocus.call(this.player, this.target, this.active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ function toggleFallback(toggle = false) {
|
|||||||
document.body.style.overflow = toggle ? 'hidden' : '';
|
document.body.style.overflow = toggle ? 'hidden' : '';
|
||||||
|
|
||||||
// Toggle class hook
|
// Toggle class hook
|
||||||
utils.toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
|
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
|
||||||
|
|
||||||
// Toggle button and fire events
|
// Toggle button and fire events
|
||||||
onChange.call(this);
|
onChange.call(this);
|
||||||
@@ -62,15 +63,20 @@ class Fullscreen {
|
|||||||
|
|
||||||
// Register event listeners
|
// Register event listeners
|
||||||
// Handle event (incase user presses escape etc)
|
// Handle event (incase user presses escape etc)
|
||||||
utils.on(document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {
|
on.call(
|
||||||
// TODO: Filter for target??
|
this.player,
|
||||||
onChange.call(this);
|
document,
|
||||||
});
|
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
|
||||||
|
() => {
|
||||||
|
// TODO: Filter for target??
|
||||||
|
onChange.call(this);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Fullscreen toggle on double click
|
// Fullscreen toggle on double click
|
||||||
utils.on(this.player.elements.container, 'dblclick', event => {
|
on.call(this.player, this.player.elements.container, 'dblclick', event => {
|
||||||
// Ignore double click in controls
|
// Ignore double click in controls
|
||||||
if (utils.is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
|
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,26 +89,27 @@ class Fullscreen {
|
|||||||
|
|
||||||
// Determine if native supported
|
// Determine if native supported
|
||||||
static get native() {
|
static get native() {
|
||||||
return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);
|
return !!(
|
||||||
|
document.fullscreenEnabled ||
|
||||||
|
document.webkitFullscreenEnabled ||
|
||||||
|
document.mozFullScreenEnabled ||
|
||||||
|
document.msFullscreenEnabled
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the prefix for handlers
|
// Get the prefix for handlers
|
||||||
static get prefix() {
|
static get prefix() {
|
||||||
// No prefix
|
// No prefix
|
||||||
if (utils.is.function(document.exitFullscreen)) {
|
if (is.function(document.exitFullscreen)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for fullscreen support by vendor prefix
|
// Check for fullscreen support by vendor prefix
|
||||||
let value = '';
|
let value = '';
|
||||||
const prefixes = [
|
const prefixes = ['webkit', 'moz', 'ms'];
|
||||||
'webkit',
|
|
||||||
'moz',
|
|
||||||
'ms',
|
|
||||||
];
|
|
||||||
|
|
||||||
prefixes.some(pre => {
|
prefixes.some(pre => {
|
||||||
if (utils.is.function(document[`${pre}ExitFullscreen`]) || utils.is.function(document[`${pre}CancelFullScreen`])) {
|
if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
|
||||||
value = pre;
|
value = pre;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -135,7 +142,7 @@ class Fullscreen {
|
|||||||
|
|
||||||
// Fallback using classname
|
// Fallback using classname
|
||||||
if (!Fullscreen.native) {
|
if (!Fullscreen.native) {
|
||||||
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
|
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
|
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
|
||||||
@@ -145,7 +152,9 @@ class Fullscreen {
|
|||||||
|
|
||||||
// Get target element
|
// Get target element
|
||||||
get target() {
|
get target() {
|
||||||
return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container;
|
return browser.isIos && this.player.config.fullscreen.iosNative
|
||||||
|
? this.player.media
|
||||||
|
: this.player.elements.container;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
@@ -157,7 +166,7 @@ class Fullscreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add styling hook to show button
|
// Add styling hook to show button
|
||||||
utils.toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
|
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make an element fullscreen
|
// Make an element fullscreen
|
||||||
@@ -175,7 +184,7 @@ class Fullscreen {
|
|||||||
toggleFallback.call(this, true);
|
toggleFallback.call(this, true);
|
||||||
} else if (!this.prefix) {
|
} else if (!this.prefix) {
|
||||||
this.target.requestFullscreen();
|
this.target.requestFullscreen();
|
||||||
} else if (!utils.is.empty(this.prefix)) {
|
} else if (!is.empty(this.prefix)) {
|
||||||
this.target[`${this.prefix}Request${this.property}`]();
|
this.target[`${this.prefix}Request${this.property}`]();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,7 +203,7 @@ class Fullscreen {
|
|||||||
toggleFallback.call(this, false);
|
toggleFallback.call(this, false);
|
||||||
} else if (!this.prefix) {
|
} else if (!this.prefix) {
|
||||||
(document.cancelFullScreen || document.exitFullscreen).call(document);
|
(document.cancelFullScreen || document.exitFullscreen).call(document);
|
||||||
} else if (!utils.is.empty(this.prefix)) {
|
} else if (!is.empty(this.prefix)) {
|
||||||
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
|
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
|
||||||
document[`${this.prefix}${action}${this.property}`]();
|
document[`${this.prefix}${action}${this.property}`]();
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-72
@@ -3,40 +3,28 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import utils from './utils';
|
import { removeElement } from './utils/elements';
|
||||||
|
import { triggerEvent } from './utils/events';
|
||||||
|
|
||||||
const html5 = {
|
const html5 = {
|
||||||
getSources() {
|
getSources() {
|
||||||
if (!this.isHTML5) {
|
if (!this.isHTML5) {
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.media.querySelectorAll('source');
|
const sources = Array.from(this.media.querySelectorAll('source'));
|
||||||
|
|
||||||
|
// Filter out unsupported sources
|
||||||
|
return sources.filter(source => support.mime.call(this, source.getAttribute('type')));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get quality levels
|
// Get quality levels
|
||||||
getQualityOptions() {
|
getQualityOptions() {
|
||||||
if (!this.isHTML5) {
|
// Get sizes from <source> elements
|
||||||
return null;
|
return html5.getSources
|
||||||
}
|
.call(this)
|
||||||
|
.map(source => Number(source.getAttribute('size')))
|
||||||
// Get sources
|
.filter(Boolean);
|
||||||
const sources = html5.getSources.call(this);
|
|
||||||
|
|
||||||
if (utils.is.empty(sources)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get <source> with size attribute
|
|
||||||
const sizes = Array.from(sources).filter(source => !utils.is.empty(source.getAttribute('size')));
|
|
||||||
|
|
||||||
// If none, bail
|
|
||||||
if (utils.is.empty(sizes)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reduce to unique list
|
|
||||||
return utils.dedupe(sizes.map(source => Number(source.getAttribute('size'))));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
extend() {
|
extend() {
|
||||||
@@ -51,71 +39,47 @@ const html5 = {
|
|||||||
get() {
|
get() {
|
||||||
// Get sources
|
// Get sources
|
||||||
const sources = html5.getSources.call(player);
|
const sources = html5.getSources.call(player);
|
||||||
|
const source = sources.find(source => source.getAttribute('src') === player.source);
|
||||||
|
|
||||||
if (utils.is.empty(sources)) {
|
// Return size, if match is found
|
||||||
return null;
|
return source && Number(source.getAttribute('size'));
|
||||||
}
|
|
||||||
|
|
||||||
const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
|
|
||||||
|
|
||||||
if (utils.is.empty(matches)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Number(matches[0].getAttribute('size'));
|
|
||||||
},
|
},
|
||||||
set(input) {
|
set(input) {
|
||||||
// Get sources
|
// Get sources
|
||||||
const sources = html5.getSources.call(player);
|
const sources = html5.getSources.call(player);
|
||||||
|
|
||||||
if (utils.is.empty(sources)) {
|
// Get first match for requested size
|
||||||
|
const source = sources.find(source => Number(source.getAttribute('size')) === input);
|
||||||
|
|
||||||
|
// No matching source found
|
||||||
|
if (!source) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get matches for requested size
|
|
||||||
const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input);
|
|
||||||
|
|
||||||
// No matches for requested size
|
|
||||||
if (utils.is.empty(matches)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get supported sources
|
|
||||||
const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type')));
|
|
||||||
|
|
||||||
// No supported sources
|
|
||||||
if (utils.is.empty(supported)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger change event
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
|
|
||||||
quality: input,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get current state
|
// Get current state
|
||||||
const { currentTime, playing } = player;
|
const { currentTime, paused, preload, readyState } = player.media;
|
||||||
|
|
||||||
// Set new source
|
// Set new source
|
||||||
player.media.src = supported[0].getAttribute('src');
|
player.media.src = source.getAttribute('src');
|
||||||
|
|
||||||
// Restore time
|
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
|
||||||
const onLoadedMetaData = () => {
|
if (preload !== 'none' || readyState) {
|
||||||
player.currentTime = currentTime;
|
// Restore time
|
||||||
player.off('loadedmetadata', onLoadedMetaData);
|
player.once('loadedmetadata', () => {
|
||||||
};
|
player.currentTime = currentTime;
|
||||||
player.on('loadedmetadata', onLoadedMetaData);
|
|
||||||
|
|
||||||
// Load new source
|
// Resume playing
|
||||||
player.media.load();
|
if (!paused) {
|
||||||
|
player.play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Resume playing
|
// Load new source
|
||||||
if (playing) {
|
player.media.load();
|
||||||
player.play();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger change event
|
// Trigger change event
|
||||||
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
|
triggerEvent.call(player, player.media, 'qualitychange', false, {
|
||||||
quality: input,
|
quality: input,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -130,7 +94,7 @@ const html5 = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove child sources
|
// Remove child sources
|
||||||
utils.removeElement(html5.getSources());
|
removeElement(html5.getSources.call(this));
|
||||||
|
|
||||||
// Set blank video src attribute
|
// Set blank video src attribute
|
||||||
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
|
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
|
||||||
|
|||||||
+8
-9
@@ -2,17 +2,19 @@
|
|||||||
// Plyr internationalization
|
// Plyr internationalization
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import utils from './utils';
|
import is from './utils/is';
|
||||||
|
import { getDeep } from './utils/objects';
|
||||||
|
import { replaceAll } from './utils/strings';
|
||||||
|
|
||||||
const i18n = {
|
const i18n = {
|
||||||
get(key = '', config = {}) {
|
get(key = '', config = {}) {
|
||||||
if (utils.is.empty(key) || utils.is.empty(config)) {
|
if (is.empty(key) || is.empty(config)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
let string = utils.getDeep(config.i18n, key);
|
let string = getDeep(config.i18n, key);
|
||||||
|
|
||||||
if (utils.is.empty(string)) {
|
if (is.empty(string)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,11 +23,8 @@ const i18n = {
|
|||||||
'{title}': config.title,
|
'{title}': config.title,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(replace).forEach(([
|
Object.entries(replace).forEach(([key, value]) => {
|
||||||
key,
|
string = replaceAll(string, key, value);
|
||||||
value,
|
|
||||||
]) => {
|
|
||||||
string = utils.replaceAll(string, key, value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return string;
|
return string;
|
||||||
|
|||||||
+139
-184
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
import controls from './controls';
|
import controls from './controls';
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
import utils from './utils';
|
import browser from './utils/browser';
|
||||||
|
import { getElement, getElements, getFocusElement, matches, toggleClass, toggleHidden } from './utils/elements';
|
||||||
// Sniff out the browser
|
import { on, once, toggleListener, triggerEvent } from './utils/events';
|
||||||
const browser = utils.getBrowser();
|
import is from './utils/is';
|
||||||
|
|
||||||
class Listeners {
|
class Listeners {
|
||||||
constructor(player) {
|
constructor(player) {
|
||||||
@@ -32,7 +32,7 @@ class Listeners {
|
|||||||
|
|
||||||
// If the event is bubbled from the media element
|
// If the event is bubbled from the media element
|
||||||
// Firefox doesn't get the keycode for whatever reason
|
// Firefox doesn't get the keycode for whatever reason
|
||||||
if (!utils.is.number(code)) {
|
if (!is.number(code)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,37 +46,16 @@ class Listeners {
|
|||||||
// Reset on keyup
|
// Reset on keyup
|
||||||
if (pressed) {
|
if (pressed) {
|
||||||
// Which keycodes should we prevent default
|
// Which keycodes should we prevent default
|
||||||
const preventDefault = [
|
const preventDefault = [32, 37, 38, 39, 40, 48, 49, 50, 51, 52, 53, 54, 56, 57, 67, 70, 73, 75, 76, 77, 79];
|
||||||
48,
|
|
||||||
49,
|
|
||||||
50,
|
|
||||||
51,
|
|
||||||
52,
|
|
||||||
53,
|
|
||||||
54,
|
|
||||||
56,
|
|
||||||
57,
|
|
||||||
32,
|
|
||||||
75,
|
|
||||||
38,
|
|
||||||
40,
|
|
||||||
77,
|
|
||||||
39,
|
|
||||||
37,
|
|
||||||
70,
|
|
||||||
67,
|
|
||||||
73,
|
|
||||||
76,
|
|
||||||
79,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Check focused element
|
// Check focused element
|
||||||
// and if the focused element is not editable (e.g. text input)
|
// and if the focused element is not editable (e.g. text input)
|
||||||
// and any that accept key input http://webaim.org/techniques/keyboard/
|
// and any that accept key input http://webaim.org/techniques/keyboard/
|
||||||
const focused = utils.getFocusElement();
|
const focused = getFocusElement();
|
||||||
if (utils.is.element(focused) && (
|
if (
|
||||||
focused !== this.player.elements.inputs.seek &&
|
is.element(focused) &&
|
||||||
utils.matches(focused, this.player.config.selectors.editable))
|
(focused !== this.player.elements.inputs.seek &&
|
||||||
|
matches(focused, this.player.config.selectors.editable))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -195,41 +174,37 @@ class Listeners {
|
|||||||
this.player.touch = true;
|
this.player.touch = true;
|
||||||
|
|
||||||
// Add touch class
|
// Add touch class
|
||||||
utils.toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
|
toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
|
||||||
|
|
||||||
// Clean up
|
|
||||||
utils.off(document.body, 'touchstart', this.firstTouch);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global window & document listeners
|
// Global window & document listeners
|
||||||
global(toggle = true) {
|
global(toggle = true) {
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
if (this.player.config.keyboard.global) {
|
if (this.player.config.keyboard.global) {
|
||||||
utils.toggleListener(window, 'keydown keyup', this.handleKey, toggle, false);
|
toggleListener.call(this.player, window, 'keydown keyup', this.handleKey, toggle, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click anywhere closes menu
|
// Click anywhere closes menu
|
||||||
utils.toggleListener(document.body, 'click', this.toggleMenu, toggle);
|
toggleListener.call(this.player, document.body, 'click', this.toggleMenu, toggle);
|
||||||
|
|
||||||
// Detect touch by events
|
// Detect touch by events
|
||||||
utils.on(document.body, 'touchstart', this.firstTouch);
|
once.call(this.player, document.body, 'touchstart', this.firstTouch);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Container listeners
|
// Container listeners
|
||||||
container() {
|
container() {
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) {
|
if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) {
|
||||||
utils.on(this.player.elements.container, 'keydown keyup', this.handleKey, false);
|
on.call(this.player, this.player.elements.container, 'keydown keyup', this.handleKey, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect tab focus
|
// Detect tab focus
|
||||||
// Remove class on blur/focusout
|
// Remove class on blur/focusout
|
||||||
utils.on(this.player.elements.container, 'focusout', event => {
|
on.call(this.player, this.player.elements.container, 'focusout', event => {
|
||||||
utils.toggleClass(event.target, this.player.config.classNames.tabFocus, false);
|
toggleClass(event.target, this.player.config.classNames.tabFocus, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add classname to tabbed elements
|
// Add classname to tabbed elements
|
||||||
utils.on(this.player.elements.container, 'keydown', event => {
|
on.call(this.player, this.player.elements.container, 'keydown', event => {
|
||||||
if (event.keyCode !== 9) {
|
if (event.keyCode !== 9) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -237,59 +212,64 @@ class Listeners {
|
|||||||
// Delay the adding of classname until the focus has changed
|
// Delay the adding of classname until the focus has changed
|
||||||
// This event fires before the focusin event
|
// This event fires before the focusin event
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
utils.toggleClass(utils.getFocusElement(), this.player.config.classNames.tabFocus, true);
|
toggleClass(getFocusElement(), this.player.config.classNames.tabFocus, true);
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle controls on mouse events and entering fullscreen
|
// Toggle controls on mouse events and entering fullscreen
|
||||||
utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {
|
on.call(
|
||||||
const { controls } = this.player.elements;
|
this.player,
|
||||||
|
this.player.elements.container,
|
||||||
|
'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',
|
||||||
|
event => {
|
||||||
|
const { controls } = this.player.elements;
|
||||||
|
|
||||||
// Remove button states for fullscreen
|
// Remove button states for fullscreen
|
||||||
if (event.type === 'enterfullscreen') {
|
if (event.type === 'enterfullscreen') {
|
||||||
controls.pressed = false;
|
controls.pressed = false;
|
||||||
controls.hover = false;
|
controls.hover = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show, then hide after a timeout unless another control event occurs
|
// Show, then hide after a timeout unless another control event occurs
|
||||||
const show = [
|
const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
|
||||||
'touchstart',
|
|
||||||
'touchmove',
|
|
||||||
'mousemove',
|
|
||||||
].includes(event.type);
|
|
||||||
|
|
||||||
let delay = 0;
|
let delay = 0;
|
||||||
|
|
||||||
if (show) {
|
if (show) {
|
||||||
ui.toggleControls.call(this.player, true);
|
ui.toggleControls.call(this.player, true);
|
||||||
// Use longer timeout for touch devices
|
// Use longer timeout for touch devices
|
||||||
delay = this.player.touch ? 3000 : 2000;
|
delay = this.player.touch ? 3000 : 2000;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear timer
|
// Clear timer
|
||||||
clearTimeout(this.player.timers.controls);
|
clearTimeout(this.player.timers.controls);
|
||||||
// Timer to prevent flicker when seeking
|
// Timer to prevent flicker when seeking
|
||||||
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
|
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for media events
|
// Listen for media events
|
||||||
media() {
|
media() {
|
||||||
// Time change on media
|
// Time change on media
|
||||||
utils.on(this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event));
|
on.call(this.player, this.player.media, 'timeupdate seeking seeked', event =>
|
||||||
|
controls.timeUpdate.call(this.player, event),
|
||||||
|
);
|
||||||
|
|
||||||
// Display duration
|
// Display duration
|
||||||
utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event));
|
on.call(this.player, this.player.media, 'durationchange loadeddata loadedmetadata', event =>
|
||||||
|
controls.durationUpdate.call(this.player, event),
|
||||||
|
);
|
||||||
|
|
||||||
// Check for audio tracks on load
|
// Check for audio tracks on load
|
||||||
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
|
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
|
||||||
utils.on(this.player.media, 'loadeddata', () => {
|
on.call(this.player, this.player.media, 'canplay', () => {
|
||||||
utils.toggleHidden(this.player.elements.volume, !this.player.hasAudio);
|
toggleHidden(this.player.elements.volume, !this.player.hasAudio);
|
||||||
utils.toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio);
|
toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle the media finishing
|
// Handle the media finishing
|
||||||
utils.on(this.player.media, 'ended', () => {
|
on.call(this.player, this.player.media, 'ended', () => {
|
||||||
// Show poster on end
|
// Show poster on end
|
||||||
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
|
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
|
||||||
// Restart
|
// Restart
|
||||||
@@ -298,20 +278,28 @@ class Listeners {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Check for buffer progress
|
// Check for buffer progress
|
||||||
utils.on(this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event));
|
on.call(this.player, this.player.media, 'progress playing seeking seeked', event =>
|
||||||
|
controls.updateProgress.call(this.player, event),
|
||||||
|
);
|
||||||
|
|
||||||
// Handle volume changes
|
// Handle volume changes
|
||||||
utils.on(this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event));
|
on.call(this.player, this.player.media, 'volumechange', event =>
|
||||||
|
controls.updateVolume.call(this.player, event),
|
||||||
|
);
|
||||||
|
|
||||||
// Handle play/pause
|
// Handle play/pause
|
||||||
utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event));
|
on.call(this.player, this.player.media, 'playing play pause ended emptied timeupdate', event =>
|
||||||
|
ui.checkPlaying.call(this.player, event),
|
||||||
|
);
|
||||||
|
|
||||||
// Loading state
|
// Loading state
|
||||||
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
|
on.call(this.player, this.player.media, 'waiting canplay seeked playing', event =>
|
||||||
|
ui.checkLoading.call(this.player, event),
|
||||||
|
);
|
||||||
|
|
||||||
// If autoplay, then load advertisement if required
|
// If autoplay, then load advertisement if required
|
||||||
// TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
|
// TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
|
||||||
utils.on(this.player.media, 'playing', () => {
|
on.call(this.player, this.player.media, 'playing', () => {
|
||||||
if (!this.player.ads) {
|
if (!this.player.ads) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -326,15 +314,15 @@ class Listeners {
|
|||||||
// Click video
|
// Click video
|
||||||
if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) {
|
if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) {
|
||||||
// Re-fetch the wrapper
|
// Re-fetch the wrapper
|
||||||
const wrapper = utils.getElement.call(this.player, `.${this.player.config.classNames.video}`);
|
const wrapper = getElement.call(this.player, `.${this.player.config.classNames.video}`);
|
||||||
|
|
||||||
// Bail if there's no wrapper (this should never happen)
|
// Bail if there's no wrapper (this should never happen)
|
||||||
if (!utils.is.element(wrapper)) {
|
if (!is.element(wrapper)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// On click play, pause ore restart
|
// On click play, pause ore restart
|
||||||
utils.on(wrapper, 'click', () => {
|
on.call(this.player, wrapper, 'click', () => {
|
||||||
// Touch devices will just show controls (if we're hiding controls)
|
// Touch devices will just show controls (if we're hiding controls)
|
||||||
if (this.player.config.hideControls && this.player.touch && !this.player.paused) {
|
if (this.player.config.hideControls && this.player.touch && !this.player.paused) {
|
||||||
return;
|
return;
|
||||||
@@ -353,7 +341,8 @@ class Listeners {
|
|||||||
|
|
||||||
// Disable right click
|
// Disable right click
|
||||||
if (this.player.supported.ui && this.player.config.disableContextMenu) {
|
if (this.player.supported.ui && this.player.config.disableContextMenu) {
|
||||||
utils.on(
|
on.call(
|
||||||
|
this.player,
|
||||||
this.player.elements.wrapper,
|
this.player.elements.wrapper,
|
||||||
'contextmenu',
|
'contextmenu',
|
||||||
event => {
|
event => {
|
||||||
@@ -364,13 +353,13 @@ class Listeners {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Volume change
|
// Volume change
|
||||||
utils.on(this.player.media, 'volumechange', () => {
|
on.call(this.player, this.player.media, 'volumechange', () => {
|
||||||
// Save to storage
|
// Save to storage
|
||||||
this.player.storage.set({ volume: this.player.volume, muted: this.player.muted });
|
this.player.storage.set({ volume: this.player.volume, muted: this.player.muted });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Speed change
|
// Speed change
|
||||||
utils.on(this.player.media, 'ratechange', () => {
|
on.call(this.player, this.player.media, 'ratechange', () => {
|
||||||
// Update UI
|
// Update UI
|
||||||
controls.updateSetting.call(this.player, 'speed');
|
controls.updateSetting.call(this.player, 'speed');
|
||||||
|
|
||||||
@@ -379,49 +368,29 @@ class Listeners {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Quality request
|
// Quality request
|
||||||
utils.on(this.player.media, 'qualityrequested', event => {
|
on.call(this.player, this.player.media, 'qualityrequested', event => {
|
||||||
// Save to storage
|
// Save to storage
|
||||||
this.player.storage.set({ quality: event.detail.quality });
|
this.player.storage.set({ quality: event.detail.quality });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Quality change
|
// Quality change
|
||||||
utils.on(this.player.media, 'qualitychange', event => {
|
on.call(this.player, this.player.media, 'qualitychange', event => {
|
||||||
// Update UI
|
// Update UI
|
||||||
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
|
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Caption language change
|
|
||||||
utils.on(this.player.media, 'languagechange', () => {
|
|
||||||
// Update UI
|
|
||||||
controls.updateSetting.call(this.player, 'captions');
|
|
||||||
|
|
||||||
// Save to storage
|
|
||||||
this.player.storage.set({ language: this.player.language });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Captions toggle
|
|
||||||
utils.on(this.player.media, 'captionsenabled captionsdisabled', () => {
|
|
||||||
// Update UI
|
|
||||||
controls.updateSetting.call(this.player, 'captions');
|
|
||||||
|
|
||||||
// Save to storage
|
|
||||||
this.player.storage.set({ captions: this.player.captions.active });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Proxy events to container
|
// Proxy events to container
|
||||||
// Bubble up key events for Edge
|
// Bubble up key events for Edge
|
||||||
utils.on(this.player.media, this.player.config.events.concat([
|
const proxyEvents = this.player.config.events.concat(['keyup', 'keydown']).join(' ');
|
||||||
'keyup',
|
on.call(this.player, this.player.media, proxyEvents, event => {
|
||||||
'keydown',
|
let { detail = {} } = event;
|
||||||
]).join(' '), event => {
|
|
||||||
let {detail = {}} = event;
|
|
||||||
|
|
||||||
// Get error details from media
|
// Get error details from media
|
||||||
if (event.type === 'error') {
|
if (event.type === 'error') {
|
||||||
detail = this.player.media.error;
|
detail = this.player.media.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.dispatchEvent.call(this.player, this.player.elements.container, event.type, true, detail);
|
triggerEvent.call(this.player, this.player.elements.container, event.type, true, detail);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +402,7 @@ class Listeners {
|
|||||||
// Run default and custom handlers
|
// Run default and custom handlers
|
||||||
const proxy = (event, defaultHandler, customHandlerKey) => {
|
const proxy = (event, defaultHandler, customHandlerKey) => {
|
||||||
const customHandler = this.player.config.listeners[customHandlerKey];
|
const customHandler = this.player.config.listeners[customHandlerKey];
|
||||||
const hasCustomHandler = utils.is.function(customHandler);
|
const hasCustomHandler = is.function(customHandler);
|
||||||
let returned = true;
|
let returned = true;
|
||||||
|
|
||||||
// Execute custom handler
|
// Execute custom handler
|
||||||
@@ -442,33 +411,43 @@ class Listeners {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only call default handler if not prevented in custom handler
|
// Only call default handler if not prevented in custom handler
|
||||||
if (returned && utils.is.function(defaultHandler)) {
|
if (returned && is.function(defaultHandler)) {
|
||||||
defaultHandler.call(this.player, event);
|
defaultHandler.call(this.player, event);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Trigger custom and default handlers
|
// Trigger custom and default handlers
|
||||||
const on = (element, type, defaultHandler, customHandlerKey, passive = true) => {
|
const bind = (element, type, defaultHandler, customHandlerKey, passive = true) => {
|
||||||
const customHandler = this.player.config.listeners[customHandlerKey];
|
const customHandler = this.player.config.listeners[customHandlerKey];
|
||||||
const hasCustomHandler = utils.is.function(customHandler);
|
const hasCustomHandler = is.function(customHandler);
|
||||||
|
|
||||||
utils.on(element, type, event => proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);
|
on.call(
|
||||||
|
this.player,
|
||||||
|
element,
|
||||||
|
type,
|
||||||
|
event => proxy(event, defaultHandler, customHandlerKey),
|
||||||
|
passive && !hasCustomHandler,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Play/pause toggle
|
// Play/pause toggle
|
||||||
on(this.player.elements.buttons.play, 'click', this.player.togglePlay, 'play');
|
if (this.player.elements.buttons.play) {
|
||||||
|
Array.from(this.player.elements.buttons.play).forEach(button => {
|
||||||
|
bind(button, 'click', this.player.togglePlay, 'play');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Pause
|
// Pause
|
||||||
on(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
|
bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
|
||||||
|
|
||||||
// Rewind
|
// Rewind
|
||||||
on(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind');
|
bind(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind');
|
||||||
|
|
||||||
// Rewind
|
// Rewind
|
||||||
on(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward');
|
bind(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward');
|
||||||
|
|
||||||
// Mute toggle
|
// Mute toggle
|
||||||
on(
|
bind(
|
||||||
this.player.elements.buttons.mute,
|
this.player.elements.buttons.mute,
|
||||||
'click',
|
'click',
|
||||||
() => {
|
() => {
|
||||||
@@ -478,10 +457,10 @@ class Listeners {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Captions toggle
|
// Captions toggle
|
||||||
on(this.player.elements.buttons.captions, 'click', this.player.toggleCaptions);
|
bind(this.player.elements.buttons.captions, 'click', () => this.player.toggleCaptions());
|
||||||
|
|
||||||
// Fullscreen toggle
|
// Fullscreen toggle
|
||||||
on(
|
bind(
|
||||||
this.player.elements.buttons.fullscreen,
|
this.player.elements.buttons.fullscreen,
|
||||||
'click',
|
'click',
|
||||||
() => {
|
() => {
|
||||||
@@ -491,7 +470,7 @@ class Listeners {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Picture-in-Picture
|
// Picture-in-Picture
|
||||||
on(
|
bind(
|
||||||
this.player.elements.buttons.pip,
|
this.player.elements.buttons.pip,
|
||||||
'click',
|
'click',
|
||||||
() => {
|
() => {
|
||||||
@@ -501,15 +480,15 @@ class Listeners {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Airplay
|
// Airplay
|
||||||
on(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay');
|
bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay');
|
||||||
|
|
||||||
// Settings menu
|
// Settings menu
|
||||||
on(this.player.elements.buttons.settings, 'click', event => {
|
bind(this.player.elements.buttons.settings, 'click', event => {
|
||||||
controls.toggleMenu.call(this.player, event);
|
controls.toggleMenu.call(this.player, event);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Settings menu
|
// Settings menu
|
||||||
on(this.player.elements.settings.form, 'click', event => {
|
bind(this.player.elements.settings.form, 'click', event => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
// Go back to home tab on click
|
// Go back to home tab on click
|
||||||
@@ -519,7 +498,7 @@ class Listeners {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Settings menu items - use event delegation as items are added/removed
|
// Settings menu items - use event delegation as items are added/removed
|
||||||
if (utils.matches(event.target, this.player.config.selectors.inputs.language)) {
|
if (matches(event.target, this.player.config.selectors.inputs.language)) {
|
||||||
proxy(
|
proxy(
|
||||||
event,
|
event,
|
||||||
() => {
|
() => {
|
||||||
@@ -528,7 +507,7 @@ class Listeners {
|
|||||||
},
|
},
|
||||||
'language',
|
'language',
|
||||||
);
|
);
|
||||||
} else if (utils.matches(event.target, this.player.config.selectors.inputs.quality)) {
|
} else if (matches(event.target, this.player.config.selectors.inputs.quality)) {
|
||||||
proxy(
|
proxy(
|
||||||
event,
|
event,
|
||||||
() => {
|
() => {
|
||||||
@@ -537,7 +516,7 @@ class Listeners {
|
|||||||
},
|
},
|
||||||
'quality',
|
'quality',
|
||||||
);
|
);
|
||||||
} else if (utils.matches(event.target, this.player.config.selectors.inputs.speed)) {
|
} else if (matches(event.target, this.player.config.selectors.inputs.speed)) {
|
||||||
proxy(
|
proxy(
|
||||||
event,
|
event,
|
||||||
() => {
|
() => {
|
||||||
@@ -553,14 +532,14 @@ class Listeners {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Set range input alternative "value", which matches the tooltip time (#954)
|
// Set range input alternative "value", which matches the tooltip time (#954)
|
||||||
on(this.player.elements.inputs.seek, 'mousedown mousemove', event => {
|
bind(this.player.elements.inputs.seek, 'mousedown mousemove', event => {
|
||||||
const clientRect = this.player.elements.progress.getBoundingClientRect();
|
const clientRect = this.player.elements.progress.getBoundingClientRect();
|
||||||
const percent = 100 / clientRect.width * (event.pageX - clientRect.left);
|
const percent = 100 / clientRect.width * (event.pageX - clientRect.left);
|
||||||
event.currentTarget.setAttribute('seek-value', percent);
|
event.currentTarget.setAttribute('seek-value', percent);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pause while seeking
|
// Pause while seeking
|
||||||
on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
|
bind(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
|
||||||
const seek = event.currentTarget;
|
const seek = event.currentTarget;
|
||||||
|
|
||||||
const code = event.keyCode ? event.keyCode : event.which;
|
const code = event.keyCode ? event.keyCode : event.which;
|
||||||
@@ -573,11 +552,7 @@ class Listeners {
|
|||||||
const play = seek.hasAttribute('play-on-seeked');
|
const play = seek.hasAttribute('play-on-seeked');
|
||||||
|
|
||||||
// Done seeking
|
// Done seeking
|
||||||
const done = [
|
const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
|
||||||
'mouseup',
|
|
||||||
'touchend',
|
|
||||||
'keyup',
|
|
||||||
].includes(event.type);
|
|
||||||
|
|
||||||
// If we're done seeking and it was playing, resume playback
|
// If we're done seeking and it was playing, resume playback
|
||||||
if (play && done) {
|
if (play && done) {
|
||||||
@@ -590,7 +565,7 @@ class Listeners {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Seek
|
// Seek
|
||||||
on(
|
bind(
|
||||||
this.player.elements.inputs.seek,
|
this.player.elements.inputs.seek,
|
||||||
inputEvent,
|
inputEvent,
|
||||||
event => {
|
event => {
|
||||||
@@ -599,7 +574,7 @@ class Listeners {
|
|||||||
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
|
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
|
||||||
let seekTo = seek.getAttribute('seek-value');
|
let seekTo = seek.getAttribute('seek-value');
|
||||||
|
|
||||||
if (utils.is.empty(seekTo)) {
|
if (is.empty(seekTo)) {
|
||||||
seekTo = seek.value;
|
seekTo = seek.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,8 +587,8 @@ class Listeners {
|
|||||||
|
|
||||||
// Current time invert
|
// Current time invert
|
||||||
// Only if one time element is used for both currentTime and duration
|
// Only if one time element is used for both currentTime and duration
|
||||||
if (this.player.config.toggleInvert && !utils.is.element(this.player.elements.display.duration)) {
|
if (this.player.config.toggleInvert && !is.element(this.player.elements.display.duration)) {
|
||||||
on(this.player.elements.display.currentTime, 'click', () => {
|
bind(this.player.elements.display.currentTime, 'click', () => {
|
||||||
// Do nothing if we're at the start
|
// Do nothing if we're at the start
|
||||||
if (this.player.currentTime === 0) {
|
if (this.player.currentTime === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -626,7 +601,7 @@ class Listeners {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Volume
|
// Volume
|
||||||
on(
|
bind(
|
||||||
this.player.elements.inputs.volume,
|
this.player.elements.inputs.volume,
|
||||||
inputEvent,
|
inputEvent,
|
||||||
event => {
|
event => {
|
||||||
@@ -637,33 +612,32 @@ class Listeners {
|
|||||||
|
|
||||||
// Polyfill for lower fill in <input type="range"> for webkit
|
// Polyfill for lower fill in <input type="range"> for webkit
|
||||||
if (browser.isWebkit) {
|
if (browser.isWebkit) {
|
||||||
on(utils.getElements.call(this.player, 'input[type="range"]'), 'input', event => {
|
Array.from(getElements.call(this.player, 'input[type="range"]')).forEach(element => {
|
||||||
controls.updateRangeFill.call(this.player, event.target);
|
bind(element, 'input', event => controls.updateRangeFill.call(this.player, event.target));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seek tooltip
|
// Seek tooltip
|
||||||
on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event));
|
bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', event =>
|
||||||
|
controls.updateSeekTooltip.call(this.player, event),
|
||||||
|
);
|
||||||
|
|
||||||
// Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
|
// Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
|
||||||
on(this.player.elements.controls, 'mouseenter mouseleave', event => {
|
bind(this.player.elements.controls, 'mouseenter mouseleave', event => {
|
||||||
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
|
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
|
// Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
|
||||||
on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
bind(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
||||||
this.player.elements.controls.pressed = [
|
this.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
|
||||||
'mousedown',
|
|
||||||
'touchstart',
|
|
||||||
].includes(event.type);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Focus in/out on controls
|
// Focus in/out on controls
|
||||||
on(this.player.elements.controls, 'focusin focusout', event => {
|
bind(this.player.elements.controls, 'focusin focusout', event => {
|
||||||
const { config, elements, timers } = this.player;
|
const { config, elements, timers } = this.player;
|
||||||
|
|
||||||
// Skip transition to prevent focus from scrolling the parent element
|
// Skip transition to prevent focus from scrolling the parent element
|
||||||
utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin');
|
toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin');
|
||||||
|
|
||||||
// Toggle
|
// Toggle
|
||||||
ui.toggleControls.call(this.player, event.type === 'focusin');
|
ui.toggleControls.call(this.player, event.type === 'focusin');
|
||||||
@@ -672,7 +646,7 @@ class Listeners {
|
|||||||
if (event.type === 'focusin') {
|
if (event.type === 'focusin') {
|
||||||
// Restore transition
|
// Restore transition
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
utils.toggleClass(elements.controls, config.classNames.noTransition, false);
|
toggleClass(elements.controls, config.classNames.noTransition, false);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// Delay a little more for keyboard users
|
// Delay a little more for keyboard users
|
||||||
@@ -686,40 +660,26 @@ class Listeners {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Mouse wheel for volume
|
// Mouse wheel for volume
|
||||||
on(
|
bind(
|
||||||
this.player.elements.inputs.volume,
|
this.player.elements.inputs.volume,
|
||||||
'wheel',
|
'wheel',
|
||||||
event => {
|
event => {
|
||||||
// Detect "natural" scroll - suppored on OS X Safari only
|
// Detect "natural" scroll - suppored on OS X Safari only
|
||||||
// Other browsers on OS X will be inverted until support improves
|
// Other browsers on OS X will be inverted until support improves
|
||||||
const inverted = event.webkitDirectionInvertedFromDevice;
|
const inverted = event.webkitDirectionInvertedFromDevice;
|
||||||
const step = 1 / 50;
|
|
||||||
let direction = 0;
|
|
||||||
|
|
||||||
// Scroll down (or up on natural) to decrease
|
// Get delta from event. Invert if `inverted` is true
|
||||||
if (event.deltaY < 0 || event.deltaX > 0) {
|
const [x, y] = [event.deltaX, -event.deltaY].map(value => (inverted ? -value : value));
|
||||||
if (inverted) {
|
|
||||||
this.player.decreaseVolume(step);
|
|
||||||
direction = -1;
|
|
||||||
} else {
|
|
||||||
this.player.increaseVolume(step);
|
|
||||||
direction = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll up (or down on natural) to increase
|
// Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)
|
||||||
if (event.deltaY > 0 || event.deltaX < 0) {
|
const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);
|
||||||
if (inverted) {
|
|
||||||
this.player.increaseVolume(step);
|
// Change the volume by 2%
|
||||||
direction = 1;
|
this.player.increaseVolume(direction / 50);
|
||||||
} else {
|
|
||||||
this.player.decreaseVolume(step);
|
|
||||||
direction = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't break page scrolling at max and min
|
// Don't break page scrolling at max and min
|
||||||
if ((direction === 1 && this.player.media.volume < 1) || (direction === -1 && this.player.media.volume > 0)) {
|
const { volume } = this.player.media;
|
||||||
|
if ((direction === 1 && volume < 1) || (direction === -1 && volume > 0)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -727,11 +687,6 @@ class Listeners {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset on destroy
|
|
||||||
clear() {
|
|
||||||
this.global(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Listeners;
|
export default Listeners;
|
||||||
|
|||||||
+12
-21
@@ -5,7 +5,7 @@
|
|||||||
import html5 from './html5';
|
import html5 from './html5';
|
||||||
import vimeo from './plugins/vimeo';
|
import vimeo from './plugins/vimeo';
|
||||||
import youtube from './plugins/youtube';
|
import youtube from './plugins/youtube';
|
||||||
import utils from './utils';
|
import { createElement, toggleClass, wrap } from './utils/elements';
|
||||||
|
|
||||||
const media = {
|
const media = {
|
||||||
// Setup media
|
// Setup media
|
||||||
@@ -17,50 +17,41 @@ const media = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add type class
|
// Add type class
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
|
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
|
||||||
|
|
||||||
// Add provider class
|
// Add provider class
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
|
toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
|
||||||
|
|
||||||
// Add video class for embeds
|
// Add video class for embeds
|
||||||
// This will require changes if audio embeds are added
|
// This will require changes if audio embeds are added
|
||||||
if (this.isEmbed) {
|
if (this.isEmbed) {
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
|
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject the player wrapper
|
// Inject the player wrapper
|
||||||
if (this.isVideo) {
|
if (this.isVideo) {
|
||||||
// Create the wrapper div
|
// Create the wrapper div
|
||||||
this.elements.wrapper = utils.createElement('div', {
|
this.elements.wrapper = createElement('div', {
|
||||||
class: this.config.classNames.video,
|
class: this.config.classNames.video,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wrap the video in a container
|
// Wrap the video in a container
|
||||||
utils.wrap(this.media, this.elements.wrapper);
|
wrap(this.media, this.elements.wrapper);
|
||||||
|
|
||||||
// Faux poster container
|
// Faux poster container
|
||||||
this.elements.poster = utils.createElement('div', {
|
this.elements.poster = createElement('div', {
|
||||||
class: this.config.classNames.poster,
|
class: this.config.classNames.poster,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.elements.wrapper.appendChild(this.elements.poster);
|
this.elements.wrapper.appendChild(this.elements.poster);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isEmbed) {
|
if (this.isHTML5) {
|
||||||
switch (this.provider) {
|
|
||||||
case 'youtube':
|
|
||||||
youtube.setup.call(this);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'vimeo':
|
|
||||||
vimeo.setup.call(this);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (this.isHTML5) {
|
|
||||||
html5.extend.call(this);
|
html5.extend.call(this);
|
||||||
|
} else if (this.isYouTube) {
|
||||||
|
youtube.setup.call(this);
|
||||||
|
} else if (this.isVimeo) {
|
||||||
|
vimeo.setup.call(this);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+29
-19
@@ -7,7 +7,12 @@
|
|||||||
/* global google */
|
/* global google */
|
||||||
|
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
import utils from '../utils';
|
import { createElement } from '../utils/elements';
|
||||||
|
import { triggerEvent } from '../utils/events';
|
||||||
|
import is from '../utils/is';
|
||||||
|
import loadScript from '../utils/loadScript';
|
||||||
|
import { formatTime } from '../utils/time';
|
||||||
|
import { buildUrlParams } from '../utils/urls';
|
||||||
|
|
||||||
class Ads {
|
class Ads {
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +49,9 @@ class Ads {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get enabled() {
|
get enabled() {
|
||||||
return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId);
|
return (
|
||||||
|
this.player.isHTML5 && this.player.isVideo && this.player.config.ads.enabled && !is.empty(this.publisherId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,9 +60,8 @@ class Ads {
|
|||||||
load() {
|
load() {
|
||||||
if (this.enabled) {
|
if (this.enabled) {
|
||||||
// Check if the Google IMA3 SDK is loaded or load it ourselves
|
// Check if the Google IMA3 SDK is loaded or load it ourselves
|
||||||
if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
|
if (!is.object(window.google) || !is.object(window.google.ima)) {
|
||||||
utils
|
loadScript(this.player.config.urls.googleIMA.sdk)
|
||||||
.loadScript(this.player.config.urls.googleIMA.sdk)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.ready();
|
this.ready();
|
||||||
})
|
})
|
||||||
@@ -94,7 +100,7 @@ class Ads {
|
|||||||
const params = {
|
const params = {
|
||||||
AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
|
AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
|
||||||
AV_CHANNELID: '5a0458dc28a06145e4519d21',
|
AV_CHANNELID: '5a0458dc28a06145e4519d21',
|
||||||
AV_URL: location.hostname,
|
AV_URL: window.location.hostname,
|
||||||
cb: Date.now(),
|
cb: Date.now(),
|
||||||
AV_WIDTH: 640,
|
AV_WIDTH: 640,
|
||||||
AV_HEIGHT: 480,
|
AV_HEIGHT: 480,
|
||||||
@@ -103,7 +109,7 @@ class Ads {
|
|||||||
|
|
||||||
const base = 'https://go.aniview.com/api/adserver6/vast/';
|
const base = 'https://go.aniview.com/api/adserver6/vast/';
|
||||||
|
|
||||||
return `${base}?${utils.buildUrlParams(params)}`;
|
return `${base}?${buildUrlParams(params)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,7 +122,7 @@ class Ads {
|
|||||||
*/
|
*/
|
||||||
setupIMA() {
|
setupIMA() {
|
||||||
// Create the container for our advertisements
|
// Create the container for our advertisements
|
||||||
this.elements.container = utils.createElement('div', {
|
this.elements.container = createElement('div', {
|
||||||
class: this.player.config.classNames.ads,
|
class: this.player.config.classNames.ads,
|
||||||
});
|
});
|
||||||
this.player.elements.container.appendChild(this.elements.container);
|
this.player.elements.container.appendChild(this.elements.container);
|
||||||
@@ -146,7 +152,11 @@ class Ads {
|
|||||||
this.loader = new google.ima.AdsLoader(this.elements.displayContainer);
|
this.loader = new google.ima.AdsLoader(this.elements.displayContainer);
|
||||||
|
|
||||||
// Listen and respond to ads loaded and error events
|
// Listen and respond to ads loaded and error events
|
||||||
this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false);
|
this.loader.addEventListener(
|
||||||
|
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
|
||||||
|
event => this.onAdsManagerLoaded(event),
|
||||||
|
false,
|
||||||
|
);
|
||||||
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);
|
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);
|
||||||
|
|
||||||
// Request video ads
|
// Request video ads
|
||||||
@@ -184,7 +194,7 @@ class Ads {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
const time = utils.formatTime(Math.max(this.manager.getRemainingTime(), 0));
|
const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));
|
||||||
const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;
|
const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;
|
||||||
this.elements.container.setAttribute('data-badge-text', label);
|
this.elements.container.setAttribute('data-badge-text', label);
|
||||||
};
|
};
|
||||||
@@ -212,14 +222,14 @@ class Ads {
|
|||||||
this.cuePoints = this.manager.getCuePoints();
|
this.cuePoints = this.manager.getCuePoints();
|
||||||
|
|
||||||
// Add advertisement cue's within the time line if available
|
// Add advertisement cue's within the time line if available
|
||||||
if (!utils.is.empty(this.cuePoints)) {
|
if (!is.empty(this.cuePoints)) {
|
||||||
this.cuePoints.forEach(cuePoint => {
|
this.cuePoints.forEach(cuePoint => {
|
||||||
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
|
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
|
||||||
const seekElement = this.player.elements.progress;
|
const seekElement = this.player.elements.progress;
|
||||||
|
|
||||||
if (utils.is.element(seekElement)) {
|
if (is.element(seekElement)) {
|
||||||
const cuePercentage = 100 / this.player.duration * cuePoint;
|
const cuePercentage = 100 / this.player.duration * cuePoint;
|
||||||
const cue = utils.createElement('span', {
|
const cue = createElement('span', {
|
||||||
class: this.player.config.classNames.cues,
|
class: this.player.config.classNames.cues,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -266,7 +276,7 @@ class Ads {
|
|||||||
// Proxy event
|
// Proxy event
|
||||||
const dispatchEvent = type => {
|
const dispatchEvent = type => {
|
||||||
const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
|
const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
|
||||||
utils.dispatchEvent.call(this.player, this.player.media, event);
|
triggerEvent.call(this.player, this.player.media, event);
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
@@ -393,7 +403,7 @@ class Ads {
|
|||||||
this.player.on('seeked', () => {
|
this.player.on('seeked', () => {
|
||||||
const seekedTime = this.player.currentTime;
|
const seekedTime = this.player.currentTime;
|
||||||
|
|
||||||
if (utils.is.empty(this.cuePoints)) {
|
if (is.empty(this.cuePoints)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,9 +540,9 @@ class Ads {
|
|||||||
trigger(event, ...args) {
|
trigger(event, ...args) {
|
||||||
const handlers = this.events[event];
|
const handlers = this.events[event];
|
||||||
|
|
||||||
if (utils.is.array(handlers)) {
|
if (is.array(handlers)) {
|
||||||
handlers.forEach(handler => {
|
handlers.forEach(handler => {
|
||||||
if (utils.is.function(handler)) {
|
if (is.function(handler)) {
|
||||||
handler.apply(this, args);
|
handler.apply(this, args);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -546,7 +556,7 @@ class Ads {
|
|||||||
* @return {Ads}
|
* @return {Ads}
|
||||||
*/
|
*/
|
||||||
on(event, callback) {
|
on(event, callback) {
|
||||||
if (!utils.is.array(this.events[event])) {
|
if (!is.array(this.events[event])) {
|
||||||
this.events[event] = [];
|
this.events[event] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,7 +587,7 @@ class Ads {
|
|||||||
* @param {string} from
|
* @param {string} from
|
||||||
*/
|
*/
|
||||||
clearSafetyTimer(from) {
|
clearSafetyTimer(from) {
|
||||||
if (!utils.is.nullOrUndefined(this.safetyTimer)) {
|
if (!is.nullOrUndefined(this.safetyTimer)) {
|
||||||
this.player.debug.log(`Safety timer cleared from: ${from}`);
|
this.player.debug.log(`Safety timer cleared from: ${from}`);
|
||||||
|
|
||||||
clearTimeout(this.safetyTimer);
|
clearTimeout(this.safetyTimer);
|
||||||
|
|||||||
+71
-45
@@ -2,10 +2,37 @@
|
|||||||
// Vimeo plugin
|
// Vimeo plugin
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import captions from './../captions';
|
import captions from '../captions';
|
||||||
import controls from './../controls';
|
import controls from '../controls';
|
||||||
import ui from './../ui';
|
import ui from '../ui';
|
||||||
import utils from './../utils';
|
import { createElement, replaceElement, toggleClass } from '../utils/elements';
|
||||||
|
import { triggerEvent } from '../utils/events';
|
||||||
|
import fetch from '../utils/fetch';
|
||||||
|
import is from '../utils/is';
|
||||||
|
import loadScript from '../utils/loadScript';
|
||||||
|
import { format, stripHTML } from '../utils/strings';
|
||||||
|
import { buildUrlParams } from '../utils/urls';
|
||||||
|
|
||||||
|
// Parse Vimeo ID from URL
|
||||||
|
function parseId(url) {
|
||||||
|
if (is.empty(url)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is.number(Number(url))) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
|
||||||
|
return url.match(regex) ? RegExp.$2 : url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get aspect ratio for dimensions
|
||||||
|
function getAspectRatio(width, height) {
|
||||||
|
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
|
||||||
|
const ratio = getRatio(width, height);
|
||||||
|
return `${width / ratio}:${height / ratio}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Set playback state and trigger change (only on actual change)
|
// Set playback state and trigger change (only on actual change)
|
||||||
function assurePlaybackState(play) {
|
function assurePlaybackState(play) {
|
||||||
@@ -14,22 +41,21 @@ function assurePlaybackState(play) {
|
|||||||
}
|
}
|
||||||
if (this.media.paused === play) {
|
if (this.media.paused === play) {
|
||||||
this.media.paused = !play;
|
this.media.paused = !play;
|
||||||
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
|
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const vimeo = {
|
const vimeo = {
|
||||||
setup() {
|
setup() {
|
||||||
// Add embed class for responsive
|
// Add embed class for responsive
|
||||||
utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
|
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
|
||||||
|
|
||||||
// Set intial ratio
|
// Set intial ratio
|
||||||
vimeo.setAspectRatio.call(this);
|
vimeo.setAspectRatio.call(this);
|
||||||
|
|
||||||
// Load the API if not already
|
// Load the API if not already
|
||||||
if (!utils.is.object(window.Vimeo)) {
|
if (!is.object(window.Vimeo)) {
|
||||||
utils
|
loadScript(this.config.urls.vimeo.sdk)
|
||||||
.loadScript(this.config.urls.vimeo.sdk)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
vimeo.ready.call(this);
|
vimeo.ready.call(this);
|
||||||
})
|
})
|
||||||
@@ -44,8 +70,8 @@ const vimeo = {
|
|||||||
// Set aspect ratio
|
// Set aspect ratio
|
||||||
// For Vimeo we have an extra 300% height <div> to hide the standard controls and UI
|
// For Vimeo we have an extra 300% height <div> to hide the standard controls and UI
|
||||||
setAspectRatio(input) {
|
setAspectRatio(input) {
|
||||||
const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':');
|
const [x, y] = (is.string(input) ? input : this.config.ratio).split(':');
|
||||||
const padding = 100 / ratio[0] * ratio[1];
|
const padding = 100 / x * y;
|
||||||
this.elements.wrapper.style.paddingBottom = `${padding}%`;
|
this.elements.wrapper.style.paddingBottom = `${padding}%`;
|
||||||
|
|
||||||
if (this.supported.ui) {
|
if (this.supported.ui) {
|
||||||
@@ -73,34 +99,37 @@ const vimeo = {
|
|||||||
gesture: 'media',
|
gesture: 'media',
|
||||||
playsinline: !this.config.fullscreen.iosNative,
|
playsinline: !this.config.fullscreen.iosNative,
|
||||||
};
|
};
|
||||||
const params = utils.buildUrlParams(options);
|
const params = buildUrlParams(options);
|
||||||
|
|
||||||
// Get the source URL or ID
|
// Get the source URL or ID
|
||||||
let source = player.media.getAttribute('src');
|
let source = player.media.getAttribute('src');
|
||||||
|
|
||||||
// Get from <div> if needed
|
// Get from <div> if needed
|
||||||
if (utils.is.empty(source)) {
|
if (is.empty(source)) {
|
||||||
source = player.media.getAttribute(player.config.attributes.embed.id);
|
source = player.media.getAttribute(player.config.attributes.embed.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = utils.parseVimeoId(source);
|
const id = parseId(source);
|
||||||
|
|
||||||
// Build an iframe
|
// Build an iframe
|
||||||
const iframe = utils.createElement('iframe');
|
const iframe = createElement('iframe');
|
||||||
const src = utils.format(player.config.urls.vimeo.iframe, id, params);
|
const src = format(player.config.urls.vimeo.iframe, id, params);
|
||||||
iframe.setAttribute('src', src);
|
iframe.setAttribute('src', src);
|
||||||
iframe.setAttribute('allowfullscreen', '');
|
iframe.setAttribute('allowfullscreen', '');
|
||||||
iframe.setAttribute('allowtransparency', '');
|
iframe.setAttribute('allowtransparency', '');
|
||||||
iframe.setAttribute('allow', 'autoplay');
|
iframe.setAttribute('allow', 'autoplay');
|
||||||
|
|
||||||
|
// Get poster, if already set
|
||||||
|
const { poster } = player;
|
||||||
|
|
||||||
// Inject the package
|
// Inject the package
|
||||||
const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer });
|
const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
|
||||||
wrapper.appendChild(iframe);
|
wrapper.appendChild(iframe);
|
||||||
player.media = utils.replaceElement(wrapper, player.media);
|
player.media = replaceElement(wrapper, player.media);
|
||||||
|
|
||||||
// Get poster image
|
// Get poster image
|
||||||
utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => {
|
fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
|
||||||
if (utils.is.empty(response)) {
|
if (is.empty(response)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +140,7 @@ const vimeo = {
|
|||||||
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
|
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
|
||||||
|
|
||||||
// Set and show poster
|
// Set and show poster
|
||||||
ui.setPoster.call(player, url.href);
|
ui.setPoster.call(player, url.href).catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup instance
|
// Setup instance
|
||||||
@@ -160,7 +189,7 @@ const vimeo = {
|
|||||||
|
|
||||||
// Set seeking state and trigger event
|
// Set seeking state and trigger event
|
||||||
media.seeking = true;
|
media.seeking = true;
|
||||||
utils.dispatchEvent.call(player, media, 'seeking');
|
triggerEvent.call(player, media, 'seeking');
|
||||||
|
|
||||||
// If paused, mute until seek is complete
|
// If paused, mute until seek is complete
|
||||||
Promise.resolve(restorePause && embed.setVolume(0))
|
Promise.resolve(restorePause && embed.setVolume(0))
|
||||||
@@ -187,7 +216,7 @@ const vimeo = {
|
|||||||
.setPlaybackRate(input)
|
.setPlaybackRate(input)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
speed = input;
|
speed = input;
|
||||||
utils.dispatchEvent.call(player, player.media, 'ratechange');
|
triggerEvent.call(player, player.media, 'ratechange');
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
// Hide menu item (and menu if empty)
|
// Hide menu item (and menu if empty)
|
||||||
@@ -207,7 +236,7 @@ const vimeo = {
|
|||||||
set(input) {
|
set(input) {
|
||||||
player.embed.setVolume(input).then(() => {
|
player.embed.setVolume(input).then(() => {
|
||||||
volume = input;
|
volume = input;
|
||||||
utils.dispatchEvent.call(player, player.media, 'volumechange');
|
triggerEvent.call(player, player.media, 'volumechange');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -219,11 +248,11 @@ const vimeo = {
|
|||||||
return muted;
|
return muted;
|
||||||
},
|
},
|
||||||
set(input) {
|
set(input) {
|
||||||
const toggle = utils.is.boolean(input) ? input : false;
|
const toggle = is.boolean(input) ? input : false;
|
||||||
|
|
||||||
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
|
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
|
||||||
muted = toggle;
|
muted = toggle;
|
||||||
utils.dispatchEvent.call(player, player.media, 'volumechange');
|
triggerEvent.call(player, player.media, 'volumechange');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -235,7 +264,7 @@ const vimeo = {
|
|||||||
return loop;
|
return loop;
|
||||||
},
|
},
|
||||||
set(input) {
|
set(input) {
|
||||||
const toggle = utils.is.boolean(input) ? input : player.config.loop.active;
|
const toggle = is.boolean(input) ? input : player.config.loop.active;
|
||||||
|
|
||||||
player.embed.setLoop(toggle).then(() => {
|
player.embed.setLoop(toggle).then(() => {
|
||||||
loop = toggle;
|
loop = toggle;
|
||||||
@@ -268,11 +297,8 @@ const vimeo = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Set aspect ratio based on video size
|
// Set aspect ratio based on video size
|
||||||
Promise.all([
|
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
|
||||||
player.embed.getVideoWidth(),
|
const ratio = getAspectRatio(dimensions[0], dimensions[1]);
|
||||||
player.embed.getVideoHeight(),
|
|
||||||
]).then(dimensions => {
|
|
||||||
const ratio = utils.getAspectRatio(dimensions[0], dimensions[1]);
|
|
||||||
vimeo.setAspectRatio.call(this, ratio);
|
vimeo.setAspectRatio.call(this, ratio);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -290,13 +316,13 @@ const vimeo = {
|
|||||||
// Get current time
|
// Get current time
|
||||||
player.embed.getCurrentTime().then(value => {
|
player.embed.getCurrentTime().then(value => {
|
||||||
currentTime = value;
|
currentTime = value;
|
||||||
utils.dispatchEvent.call(player, player.media, 'timeupdate');
|
triggerEvent.call(player, player.media, 'timeupdate');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get duration
|
// Get duration
|
||||||
player.embed.getDuration().then(value => {
|
player.embed.getDuration().then(value => {
|
||||||
player.media.duration = value;
|
player.media.duration = value;
|
||||||
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
triggerEvent.call(player, player.media, 'durationchange');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get captions
|
// Get captions
|
||||||
@@ -306,7 +332,7 @@ const vimeo = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('cuechange', ({ cues = [] }) => {
|
player.embed.on('cuechange', ({ cues = [] }) => {
|
||||||
const strippedCues = cues.map(cue => utils.stripHTML(cue.text));
|
const strippedCues = cues.map(cue => stripHTML(cue.text));
|
||||||
captions.updateCues.call(player, strippedCues);
|
captions.updateCues.call(player, strippedCues);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -315,11 +341,11 @@ const vimeo = {
|
|||||||
player.embed.getPaused().then(paused => {
|
player.embed.getPaused().then(paused => {
|
||||||
assurePlaybackState.call(player, !paused);
|
assurePlaybackState.call(player, !paused);
|
||||||
if (!paused) {
|
if (!paused) {
|
||||||
utils.dispatchEvent.call(player, player.media, 'playing');
|
triggerEvent.call(player, player.media, 'playing');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (utils.is.element(player.embed.element) && player.supported.ui) {
|
if (is.element(player.embed.element) && player.supported.ui) {
|
||||||
const frame = player.embed.element;
|
const frame = player.embed.element;
|
||||||
|
|
||||||
// Fix keyboard focus issues
|
// Fix keyboard focus issues
|
||||||
@@ -330,7 +356,7 @@ const vimeo = {
|
|||||||
|
|
||||||
player.embed.on('play', () => {
|
player.embed.on('play', () => {
|
||||||
assurePlaybackState.call(player, true);
|
assurePlaybackState.call(player, true);
|
||||||
utils.dispatchEvent.call(player, player.media, 'playing');
|
triggerEvent.call(player, player.media, 'playing');
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('pause', () => {
|
player.embed.on('pause', () => {
|
||||||
@@ -340,16 +366,16 @@ const vimeo = {
|
|||||||
player.embed.on('timeupdate', data => {
|
player.embed.on('timeupdate', data => {
|
||||||
player.media.seeking = false;
|
player.media.seeking = false;
|
||||||
currentTime = data.seconds;
|
currentTime = data.seconds;
|
||||||
utils.dispatchEvent.call(player, player.media, 'timeupdate');
|
triggerEvent.call(player, player.media, 'timeupdate');
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('progress', data => {
|
player.embed.on('progress', data => {
|
||||||
player.media.buffered = data.percent;
|
player.media.buffered = data.percent;
|
||||||
utils.dispatchEvent.call(player, player.media, 'progress');
|
triggerEvent.call(player, player.media, 'progress');
|
||||||
|
|
||||||
// Check all loaded
|
// Check all loaded
|
||||||
if (parseInt(data.percent, 10) === 1) {
|
if (parseInt(data.percent, 10) === 1) {
|
||||||
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
|
triggerEvent.call(player, player.media, 'canplaythrough');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get duration as if we do it before load, it gives an incorrect value
|
// Get duration as if we do it before load, it gives an incorrect value
|
||||||
@@ -357,24 +383,24 @@ const vimeo = {
|
|||||||
player.embed.getDuration().then(value => {
|
player.embed.getDuration().then(value => {
|
||||||
if (value !== player.media.duration) {
|
if (value !== player.media.duration) {
|
||||||
player.media.duration = value;
|
player.media.duration = value;
|
||||||
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
triggerEvent.call(player, player.media, 'durationchange');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('seeked', () => {
|
player.embed.on('seeked', () => {
|
||||||
player.media.seeking = false;
|
player.media.seeking = false;
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeked');
|
triggerEvent.call(player, player.media, 'seeked');
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('ended', () => {
|
player.embed.on('ended', () => {
|
||||||
player.media.paused = true;
|
player.media.paused = true;
|
||||||
utils.dispatchEvent.call(player, player.media, 'ended');
|
triggerEvent.call(player, player.media, 'ended');
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('error', detail => {
|
player.embed.on('error', detail => {
|
||||||
player.media.error = detail;
|
player.media.error = detail;
|
||||||
utils.dispatchEvent.call(player, player.media, 'error');
|
triggerEvent.call(player, player.media, 'error');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rebuild UI
|
// Rebuild UI
|
||||||
|
|||||||
+107
-143
@@ -2,66 +2,56 @@
|
|||||||
// YouTube plugin
|
// YouTube plugin
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import controls from './../controls';
|
import controls from '../controls';
|
||||||
import ui from './../ui';
|
import ui from '../ui';
|
||||||
import utils from './../utils';
|
import { dedupe } from '../utils/arrays';
|
||||||
|
import { createElement, replaceElement, toggleClass } from '../utils/elements';
|
||||||
|
import { triggerEvent } from '../utils/events';
|
||||||
|
import fetch from '../utils/fetch';
|
||||||
|
import is from '../utils/is';
|
||||||
|
import loadImage from '../utils/loadImage';
|
||||||
|
import loadScript from '../utils/loadScript';
|
||||||
|
import { format, generateId } from '../utils/strings';
|
||||||
|
|
||||||
|
// Parse YouTube ID from URL
|
||||||
|
function parseId(url) {
|
||||||
|
if (is.empty(url)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
|
||||||
|
return url.match(regex) ? RegExp.$2 : url;
|
||||||
|
}
|
||||||
|
|
||||||
// Standardise YouTube quality unit
|
// Standardise YouTube quality unit
|
||||||
function mapQualityUnit(input) {
|
function mapQualityUnit(input) {
|
||||||
switch (input) {
|
const qualities = {
|
||||||
case 'hd2160':
|
hd2160: 2160,
|
||||||
return 2160;
|
hd1440: 1440,
|
||||||
|
hd1080: 1080,
|
||||||
|
hd720: 720,
|
||||||
|
large: 480,
|
||||||
|
medium: 360,
|
||||||
|
small: 240,
|
||||||
|
tiny: 144,
|
||||||
|
};
|
||||||
|
|
||||||
case 2160:
|
const entry = Object.entries(qualities).find(entry => entry.includes(input));
|
||||||
return 'hd2160';
|
|
||||||
|
|
||||||
case 'hd1440':
|
if (entry) {
|
||||||
return 1440;
|
// Get the match corresponding to the input
|
||||||
|
return entry.find(value => value !== input);
|
||||||
case 1440:
|
|
||||||
return 'hd1440';
|
|
||||||
|
|
||||||
case 'hd1080':
|
|
||||||
return 1080;
|
|
||||||
|
|
||||||
case 1080:
|
|
||||||
return 'hd1080';
|
|
||||||
|
|
||||||
case 'hd720':
|
|
||||||
return 720;
|
|
||||||
|
|
||||||
case 720:
|
|
||||||
return 'hd720';
|
|
||||||
|
|
||||||
case 'large':
|
|
||||||
return 480;
|
|
||||||
|
|
||||||
case 480:
|
|
||||||
return 'large';
|
|
||||||
|
|
||||||
case 'medium':
|
|
||||||
return 360;
|
|
||||||
|
|
||||||
case 360:
|
|
||||||
return 'medium';
|
|
||||||
|
|
||||||
case 'small':
|
|
||||||
return 240;
|
|
||||||
|
|
||||||
case 240:
|
|
||||||
return 'small';
|
|
||||||
|
|
||||||
default:
|
|
||||||
return 'default';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapQualityUnits(levels) {
|
function mapQualityUnits(levels) {
|
||||||
if (utils.is.empty(levels)) {
|
if (is.empty(levels)) {
|
||||||
return levels;
|
return levels;
|
||||||
}
|
}
|
||||||
|
|
||||||
return utils.dedupe(levels.map(level => mapQualityUnit(level)));
|
return dedupe(levels.map(level => mapQualityUnit(level)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set playback state and trigger change (only on actual change)
|
// Set playback state and trigger change (only on actual change)
|
||||||
@@ -71,24 +61,24 @@ function assurePlaybackState(play) {
|
|||||||
}
|
}
|
||||||
if (this.media.paused === play) {
|
if (this.media.paused === play) {
|
||||||
this.media.paused = !play;
|
this.media.paused = !play;
|
||||||
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
|
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const youtube = {
|
const youtube = {
|
||||||
setup() {
|
setup() {
|
||||||
// Add embed class for responsive
|
// Add embed class for responsive
|
||||||
utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
|
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
|
||||||
|
|
||||||
// Set aspect ratio
|
// Set aspect ratio
|
||||||
youtube.setAspectRatio.call(this);
|
youtube.setAspectRatio.call(this);
|
||||||
|
|
||||||
// Setup API
|
// Setup API
|
||||||
if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) {
|
if (is.object(window.YT) && is.function(window.YT.Player)) {
|
||||||
youtube.ready.call(this);
|
youtube.ready.call(this);
|
||||||
} else {
|
} else {
|
||||||
// Load the API
|
// Load the API
|
||||||
utils.loadScript(this.config.urls.youtube.sdk).catch(error => {
|
loadScript(this.config.urls.youtube.sdk).catch(error => {
|
||||||
this.debug.warn('YouTube API failed to load', error);
|
this.debug.warn('YouTube API failed to load', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,10 +105,10 @@ const youtube = {
|
|||||||
// Try via undocumented API method first
|
// Try via undocumented API method first
|
||||||
// This method disappears now and then though...
|
// This method disappears now and then though...
|
||||||
// https://github.com/sampotts/plyr/issues/709
|
// https://github.com/sampotts/plyr/issues/709
|
||||||
if (utils.is.function(this.embed.getVideoData)) {
|
if (is.function(this.embed.getVideoData)) {
|
||||||
const { title } = this.embed.getVideoData();
|
const { title } = this.embed.getVideoData();
|
||||||
|
|
||||||
if (utils.is.empty(title)) {
|
if (is.empty(title)) {
|
||||||
this.config.title = title;
|
this.config.title = title;
|
||||||
ui.setTitle.call(this);
|
ui.setTitle.call(this);
|
||||||
return;
|
return;
|
||||||
@@ -127,13 +117,12 @@ const youtube = {
|
|||||||
|
|
||||||
// Or via Google API
|
// Or via Google API
|
||||||
const key = this.config.keys.google;
|
const key = this.config.keys.google;
|
||||||
if (utils.is.string(key) && !utils.is.empty(key)) {
|
if (is.string(key) && !is.empty(key)) {
|
||||||
const url = utils.format(this.config.urls.youtube.api, videoId, key);
|
const url = format(this.config.urls.youtube.api, videoId, key);
|
||||||
|
|
||||||
utils
|
fetch(url)
|
||||||
.fetch(url)
|
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (utils.is.object(result)) {
|
if (is.object(result)) {
|
||||||
this.config.title = result.items[0].snippet.title;
|
this.config.title = result.items[0].snippet.title;
|
||||||
ui.setTitle.call(this);
|
ui.setTitle.call(this);
|
||||||
}
|
}
|
||||||
@@ -154,7 +143,7 @@ const youtube = {
|
|||||||
|
|
||||||
// Ignore already setup (race condition)
|
// Ignore already setup (race condition)
|
||||||
const currentId = player.media.getAttribute('id');
|
const currentId = player.media.getAttribute('id');
|
||||||
if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) {
|
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,30 +151,36 @@ const youtube = {
|
|||||||
let source = player.media.getAttribute('src');
|
let source = player.media.getAttribute('src');
|
||||||
|
|
||||||
// Get from <div> if needed
|
// Get from <div> if needed
|
||||||
if (utils.is.empty(source)) {
|
if (is.empty(source)) {
|
||||||
source = player.media.getAttribute(this.config.attributes.embed.id);
|
source = player.media.getAttribute(this.config.attributes.embed.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace the <iframe> with a <div> due to YouTube API issues
|
// Replace the <iframe> with a <div> due to YouTube API issues
|
||||||
const videoId = utils.parseYouTubeId(source);
|
const videoId = parseId(source);
|
||||||
const id = utils.generateId(player.provider);
|
const id = generateId(player.provider);
|
||||||
const container = utils.createElement('div', { id });
|
|
||||||
player.media = utils.replaceElement(container, player.media);
|
|
||||||
|
|
||||||
// Set poster image
|
// Get poster, if already set
|
||||||
|
const { poster } = player;
|
||||||
|
|
||||||
|
// Replace media element
|
||||||
|
const container = createElement('div', { id, poster });
|
||||||
|
player.media = replaceElement(container, player.media);
|
||||||
|
|
||||||
|
// Id to poster wrapper
|
||||||
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
|
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
|
||||||
|
|
||||||
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
|
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
|
||||||
utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
|
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
|
||||||
.catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
|
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
|
||||||
.catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
|
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
|
||||||
.then(image => ui.setPoster.call(player, image.src))
|
.then(image => ui.setPoster.call(player, image.src))
|
||||||
.then(posterSrc => {
|
.then(posterSrc => {
|
||||||
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
|
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
|
||||||
if (!posterSrc.includes('maxres')) {
|
if (!posterSrc.includes('maxres')) {
|
||||||
player.elements.poster.style.backgroundSize = 'cover';
|
player.elements.poster.style.backgroundSize = 'cover';
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
// Setup instance
|
// Setup instance
|
||||||
// https://developers.google.com/youtube/iframe_api_reference
|
// https://developers.google.com/youtube/iframe_api_reference
|
||||||
@@ -211,49 +206,26 @@ const youtube = {
|
|||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
onError(event) {
|
onError(event) {
|
||||||
// If we've already fired an error, don't do it again
|
// YouTube may fire onError twice, so only handle it once
|
||||||
// YouTube fires onError twice
|
if (!player.media.error) {
|
||||||
if (utils.is.object(player.media.error)) {
|
const code = event.data;
|
||||||
return;
|
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
|
||||||
|
const message =
|
||||||
|
{
|
||||||
|
2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
|
||||||
|
5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
|
||||||
|
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
|
||||||
|
101: 'The owner of the requested video does not allow it to be played in embedded players.',
|
||||||
|
150: 'The owner of the requested video does not allow it to be played in embedded players.',
|
||||||
|
}[code] || 'An unknown error occured';
|
||||||
|
|
||||||
|
player.media.error = { code, message };
|
||||||
|
|
||||||
|
triggerEvent.call(player, player.media, 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
const detail = {
|
|
||||||
code: event.data,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
|
|
||||||
switch (event.data) {
|
|
||||||
case 2:
|
|
||||||
detail.message =
|
|
||||||
'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 5:
|
|
||||||
detail.message =
|
|
||||||
'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 100:
|
|
||||||
detail.message =
|
|
||||||
'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 101:
|
|
||||||
case 150:
|
|
||||||
detail.message = 'The owner of the requested video does not allow it to be played in embedded players.';
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
detail.message = 'An unknown error occured';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
player.media.error = detail;
|
|
||||||
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'error');
|
|
||||||
},
|
},
|
||||||
onPlaybackQualityChange() {
|
onPlaybackQualityChange() {
|
||||||
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
|
triggerEvent.call(player, player.media, 'qualitychange', false, {
|
||||||
quality: player.media.quality,
|
quality: player.media.quality,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -264,7 +236,7 @@ const youtube = {
|
|||||||
// Get current speed
|
// Get current speed
|
||||||
player.media.playbackRate = instance.getPlaybackRate();
|
player.media.playbackRate = instance.getPlaybackRate();
|
||||||
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'ratechange');
|
triggerEvent.call(player, player.media, 'ratechange');
|
||||||
},
|
},
|
||||||
onReady(event) {
|
onReady(event) {
|
||||||
// Get the instance
|
// Get the instance
|
||||||
@@ -298,14 +270,14 @@ const youtube = {
|
|||||||
return Number(instance.getCurrentTime());
|
return Number(instance.getCurrentTime());
|
||||||
},
|
},
|
||||||
set(time) {
|
set(time) {
|
||||||
// If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
|
// If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
|
||||||
if (player.paused) {
|
if (player.paused && !player.embed.hasPlayed) {
|
||||||
player.embed.mute();
|
player.embed.mute();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set seeking state and trigger event
|
// Set seeking state and trigger event
|
||||||
player.media.seeking = true;
|
player.media.seeking = true;
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeking');
|
triggerEvent.call(player, player.media, 'seeking');
|
||||||
|
|
||||||
// Seek after events sent
|
// Seek after events sent
|
||||||
instance.seekTo(time);
|
instance.seekTo(time);
|
||||||
@@ -328,15 +300,7 @@ const youtube = {
|
|||||||
return mapQualityUnit(instance.getPlaybackQuality());
|
return mapQualityUnit(instance.getPlaybackQuality());
|
||||||
},
|
},
|
||||||
set(input) {
|
set(input) {
|
||||||
const quality = input;
|
instance.setPlaybackQuality(mapQualityUnit(input));
|
||||||
|
|
||||||
// Set via API
|
|
||||||
instance.setPlaybackQuality(mapQualityUnit(quality));
|
|
||||||
|
|
||||||
// Trigger request event
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
|
|
||||||
quality,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -349,7 +313,7 @@ const youtube = {
|
|||||||
set(input) {
|
set(input) {
|
||||||
volume = input;
|
volume = input;
|
||||||
instance.setVolume(volume * 100);
|
instance.setVolume(volume * 100);
|
||||||
utils.dispatchEvent.call(player, player.media, 'volumechange');
|
triggerEvent.call(player, player.media, 'volumechange');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -360,10 +324,10 @@ const youtube = {
|
|||||||
return muted;
|
return muted;
|
||||||
},
|
},
|
||||||
set(input) {
|
set(input) {
|
||||||
const toggle = utils.is.boolean(input) ? input : muted;
|
const toggle = is.boolean(input) ? input : muted;
|
||||||
muted = toggle;
|
muted = toggle;
|
||||||
instance[toggle ? 'mute' : 'unMute']();
|
instance[toggle ? 'mute' : 'unMute']();
|
||||||
utils.dispatchEvent.call(player, player.media, 'volumechange');
|
triggerEvent.call(player, player.media, 'volumechange');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -389,8 +353,8 @@ const youtube = {
|
|||||||
player.media.setAttribute('tabindex', -1);
|
player.media.setAttribute('tabindex', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'timeupdate');
|
triggerEvent.call(player, player.media, 'timeupdate');
|
||||||
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
triggerEvent.call(player, player.media, 'durationchange');
|
||||||
|
|
||||||
// Reset timer
|
// Reset timer
|
||||||
clearInterval(player.timers.buffering);
|
clearInterval(player.timers.buffering);
|
||||||
@@ -402,7 +366,7 @@ const youtube = {
|
|||||||
|
|
||||||
// Trigger progress only when we actually buffer something
|
// Trigger progress only when we actually buffer something
|
||||||
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
|
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
|
||||||
utils.dispatchEvent.call(player, player.media, 'progress');
|
triggerEvent.call(player, player.media, 'progress');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set last buffer point
|
// Set last buffer point
|
||||||
@@ -413,7 +377,7 @@ const youtube = {
|
|||||||
clearInterval(player.timers.buffering);
|
clearInterval(player.timers.buffering);
|
||||||
|
|
||||||
// Trigger event
|
// Trigger event
|
||||||
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
|
triggerEvent.call(player, player.media, 'canplaythrough');
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
@@ -427,15 +391,12 @@ const youtube = {
|
|||||||
// Reset timer
|
// Reset timer
|
||||||
clearInterval(player.timers.playing);
|
clearInterval(player.timers.playing);
|
||||||
|
|
||||||
const seeked = player.media.seeking && [
|
const seeked = player.media.seeking && [1, 2].includes(event.data);
|
||||||
1,
|
|
||||||
2,
|
|
||||||
].includes(event.data);
|
|
||||||
|
|
||||||
if (seeked) {
|
if (seeked) {
|
||||||
// Unset seeking and fire seeked event
|
// Unset seeking and fire seeked event
|
||||||
player.media.seeking = false;
|
player.media.seeking = false;
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeked');
|
triggerEvent.call(player, player.media, 'seeked');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle events
|
// Handle events
|
||||||
@@ -448,11 +409,11 @@ const youtube = {
|
|||||||
switch (event.data) {
|
switch (event.data) {
|
||||||
case -1:
|
case -1:
|
||||||
// Update scrubber
|
// Update scrubber
|
||||||
utils.dispatchEvent.call(player, player.media, 'timeupdate');
|
triggerEvent.call(player, player.media, 'timeupdate');
|
||||||
|
|
||||||
// Get loaded % from YouTube
|
// Get loaded % from YouTube
|
||||||
player.media.buffered = instance.getVideoLoadedFraction();
|
player.media.buffered = instance.getVideoLoadedFraction();
|
||||||
utils.dispatchEvent.call(player, player.media, 'progress');
|
triggerEvent.call(player, player.media, 'progress');
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -465,7 +426,7 @@ const youtube = {
|
|||||||
instance.stopVideo();
|
instance.stopVideo();
|
||||||
instance.playVideo();
|
instance.playVideo();
|
||||||
} else {
|
} else {
|
||||||
utils.dispatchEvent.call(player, player.media, 'ended');
|
triggerEvent.call(player, player.media, 'ended');
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -477,11 +438,11 @@ const youtube = {
|
|||||||
} else {
|
} else {
|
||||||
assurePlaybackState.call(player, true);
|
assurePlaybackState.call(player, true);
|
||||||
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'playing');
|
triggerEvent.call(player, player.media, 'playing');
|
||||||
|
|
||||||
// Poll to get playback progress
|
// Poll to get playback progress
|
||||||
player.timers.playing = setInterval(() => {
|
player.timers.playing = setInterval(() => {
|
||||||
utils.dispatchEvent.call(player, player.media, 'timeupdate');
|
triggerEvent.call(player, player.media, 'timeupdate');
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
// Check duration again due to YouTube bug
|
// Check duration again due to YouTube bug
|
||||||
@@ -489,11 +450,14 @@ const youtube = {
|
|||||||
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
|
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
|
||||||
if (player.media.duration !== instance.getDuration()) {
|
if (player.media.duration !== instance.getDuration()) {
|
||||||
player.media.duration = instance.getDuration();
|
player.media.duration = instance.getDuration();
|
||||||
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
triggerEvent.call(player, player.media, 'durationchange');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get quality
|
// Get quality
|
||||||
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
|
controls.setQualityMenu.call(
|
||||||
|
player,
|
||||||
|
mapQualityUnits(instance.getAvailableQualityLevels()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -511,7 +475,7 @@ const youtube = {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, {
|
triggerEvent.call(player, player.elements.container, 'statechange', false, {
|
||||||
code: event.data,
|
code: event.data,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
+140
-162
@@ -1,14 +1,15 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Plyr
|
// Plyr
|
||||||
// plyr.js v3.3.12
|
// plyr.js v3.3.22
|
||||||
// https://github.com/sampotts/plyr
|
// https://github.com/sampotts/plyr
|
||||||
// License: The MIT License (MIT)
|
// License: The MIT License (MIT)
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import captions from './captions';
|
import captions from './captions';
|
||||||
|
import defaults from './config/defaults';
|
||||||
|
import { getProviderByUrl, providers, types } from './config/types';
|
||||||
import Console from './console';
|
import Console from './console';
|
||||||
import controls from './controls';
|
import controls from './controls';
|
||||||
import defaults from './defaults';
|
|
||||||
import Fullscreen from './fullscreen';
|
import Fullscreen from './fullscreen';
|
||||||
import Listeners from './listeners';
|
import Listeners from './listeners';
|
||||||
import media from './media';
|
import media from './media';
|
||||||
@@ -16,9 +17,14 @@ import Ads from './plugins/ads';
|
|||||||
import source from './source';
|
import source from './source';
|
||||||
import Storage from './storage';
|
import Storage from './storage';
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import { providers, types } from './types';
|
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
import utils from './utils';
|
import { closest } from './utils/arrays';
|
||||||
|
import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';
|
||||||
|
import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
|
||||||
|
import is from './utils/is';
|
||||||
|
import loadSprite from './utils/loadSprite';
|
||||||
|
import { cloneDeep, extend } from './utils/objects';
|
||||||
|
import { parseUrl } from './utils/urls';
|
||||||
|
|
||||||
// Private properties
|
// Private properties
|
||||||
// TODO: Use a WeakMap for private globals
|
// TODO: Use a WeakMap for private globals
|
||||||
@@ -41,18 +47,18 @@ class Plyr {
|
|||||||
this.media = target;
|
this.media = target;
|
||||||
|
|
||||||
// String selector passed
|
// String selector passed
|
||||||
if (utils.is.string(this.media)) {
|
if (is.string(this.media)) {
|
||||||
this.media = document.querySelectorAll(this.media);
|
this.media = document.querySelectorAll(this.media);
|
||||||
}
|
}
|
||||||
|
|
||||||
// jQuery, NodeList or Array passed, use first element
|
// jQuery, NodeList or Array passed, use first element
|
||||||
if ((window.jQuery && this.media instanceof jQuery) || utils.is.nodeList(this.media) || utils.is.array(this.media)) {
|
if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
this.media = this.media[0];
|
this.media = this.media[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set config
|
// Set config
|
||||||
this.config = utils.extend(
|
this.config = extend(
|
||||||
{},
|
{},
|
||||||
defaults,
|
defaults,
|
||||||
Plyr.defaults,
|
Plyr.defaults,
|
||||||
@@ -108,7 +114,7 @@ class Plyr {
|
|||||||
this.debug.log('Support', support);
|
this.debug.log('Support', support);
|
||||||
|
|
||||||
// We need an element to setup
|
// We need an element to setup
|
||||||
if (utils.is.nullOrUndefined(this.media) || !utils.is.element(this.media)) {
|
if (is.nullOrUndefined(this.media) || !is.element(this.media)) {
|
||||||
this.debug.error('Setup failed: no suitable element passed');
|
this.debug.error('Setup failed: no suitable element passed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -144,7 +150,6 @@ class Plyr {
|
|||||||
// Embed properties
|
// Embed properties
|
||||||
let iframe = null;
|
let iframe = null;
|
||||||
let url = null;
|
let url = null;
|
||||||
let params = null;
|
|
||||||
|
|
||||||
// Different setup based on type
|
// Different setup based on type
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -153,10 +158,10 @@ class Plyr {
|
|||||||
iframe = this.media.querySelector('iframe');
|
iframe = this.media.querySelector('iframe');
|
||||||
|
|
||||||
// <iframe> type
|
// <iframe> type
|
||||||
if (utils.is.element(iframe)) {
|
if (is.element(iframe)) {
|
||||||
// Detect provider
|
// Detect provider
|
||||||
url = iframe.getAttribute('src');
|
url = parseUrl(iframe.getAttribute('src'));
|
||||||
this.provider = utils.getProviderByUrl(url);
|
this.provider = getProviderByUrl(url.toString());
|
||||||
|
|
||||||
// Rework elements
|
// Rework elements
|
||||||
this.elements.container = this.media;
|
this.elements.container = this.media;
|
||||||
@@ -166,24 +171,20 @@ class Plyr {
|
|||||||
this.elements.container.className = '';
|
this.elements.container.className = '';
|
||||||
|
|
||||||
// Get attributes from URL and set config
|
// Get attributes from URL and set config
|
||||||
params = utils.getUrlParams(url);
|
if (url.searchParams.length) {
|
||||||
if (!utils.is.empty(params)) {
|
const truthy = ['1', 'true'];
|
||||||
const truthy = [
|
|
||||||
'1',
|
|
||||||
'true',
|
|
||||||
];
|
|
||||||
|
|
||||||
if (truthy.includes(params.autoplay)) {
|
if (truthy.includes(url.searchParams.get('autoplay'))) {
|
||||||
this.config.autoplay = true;
|
this.config.autoplay = true;
|
||||||
}
|
}
|
||||||
if (truthy.includes(params.loop)) {
|
if (truthy.includes(url.searchParams.get('loop'))) {
|
||||||
this.config.loop.active = true;
|
this.config.loop.active = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: replace fullscreen.iosNative with this playsinline config option
|
// TODO: replace fullscreen.iosNative with this playsinline config option
|
||||||
// YouTube requires the playsinline in the URL
|
// YouTube requires the playsinline in the URL
|
||||||
if (this.isYouTube) {
|
if (this.isYouTube) {
|
||||||
this.config.playsinline = truthy.includes(params.playsinline);
|
this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));
|
||||||
} else {
|
} else {
|
||||||
this.config.playsinline = true;
|
this.config.playsinline = true;
|
||||||
}
|
}
|
||||||
@@ -197,7 +198,7 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unsupported or missing provider
|
// Unsupported or missing provider
|
||||||
if (utils.is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
|
if (is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
|
||||||
this.debug.error('Setup failed: Invalid provider');
|
this.debug.error('Setup failed: Invalid provider');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -245,6 +246,8 @@ class Plyr {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.eventListeners = [];
|
||||||
|
|
||||||
// Create listeners
|
// Create listeners
|
||||||
this.listeners = new Listeners(this);
|
this.listeners = new Listeners(this);
|
||||||
|
|
||||||
@@ -255,14 +258,11 @@ class Plyr {
|
|||||||
this.media.plyr = this;
|
this.media.plyr = this;
|
||||||
|
|
||||||
// Wrap media
|
// Wrap media
|
||||||
if (!utils.is.element(this.elements.container)) {
|
if (!is.element(this.elements.container)) {
|
||||||
this.elements.container = utils.createElement('div');
|
this.elements.container = createElement('div');
|
||||||
utils.wrap(this.media, this.elements.container);
|
wrap(this.media, this.elements.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow focus to be captured
|
|
||||||
this.elements.container.setAttribute('tabindex', 0);
|
|
||||||
|
|
||||||
// Add style hook
|
// Add style hook
|
||||||
ui.addStyleHook.call(this);
|
ui.addStyleHook.call(this);
|
||||||
|
|
||||||
@@ -271,7 +271,7 @@ class Plyr {
|
|||||||
|
|
||||||
// Listen for events if debugging
|
// Listen for events if debugging
|
||||||
if (this.config.debug) {
|
if (this.config.debug) {
|
||||||
utils.on(this.elements.container, this.config.events.join(' '), event => {
|
on.call(this, this.elements.container, this.config.events.join(' '), event => {
|
||||||
this.debug.log(`event: ${event.type}`);
|
this.debug.log(`event: ${event.type}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -310,18 +310,23 @@ class Plyr {
|
|||||||
get isHTML5() {
|
get isHTML5() {
|
||||||
return Boolean(this.provider === providers.html5);
|
return Boolean(this.provider === providers.html5);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isEmbed() {
|
get isEmbed() {
|
||||||
return Boolean(this.isYouTube || this.isVimeo);
|
return Boolean(this.isYouTube || this.isVimeo);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isYouTube() {
|
get isYouTube() {
|
||||||
return Boolean(this.provider === providers.youtube);
|
return Boolean(this.provider === providers.youtube);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isVimeo() {
|
get isVimeo() {
|
||||||
return Boolean(this.provider === providers.vimeo);
|
return Boolean(this.provider === providers.vimeo);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isVideo() {
|
get isVideo() {
|
||||||
return Boolean(this.type === types.video);
|
return Boolean(this.type === types.video);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isAudio() {
|
get isAudio() {
|
||||||
return Boolean(this.type === types.audio);
|
return Boolean(this.type === types.audio);
|
||||||
}
|
}
|
||||||
@@ -330,7 +335,7 @@ class Plyr {
|
|||||||
* Play the media, or play the advertisement (if they are not blocked)
|
* Play the media, or play the advertisement (if they are not blocked)
|
||||||
*/
|
*/
|
||||||
play() {
|
play() {
|
||||||
if (!utils.is.function(this.media.play)) {
|
if (!is.function(this.media.play)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,7 +347,7 @@ class Plyr {
|
|||||||
* Pause the media
|
* Pause the media
|
||||||
*/
|
*/
|
||||||
pause() {
|
pause() {
|
||||||
if (!this.playing || !utils.is.function(this.media.pause)) {
|
if (!this.playing || !is.function(this.media.pause)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,7 +388,7 @@ class Plyr {
|
|||||||
*/
|
*/
|
||||||
togglePlay(input) {
|
togglePlay(input) {
|
||||||
// Toggle based on current state if nothing passed
|
// Toggle based on current state if nothing passed
|
||||||
const toggle = utils.is.boolean(input) ? input : !this.playing;
|
const toggle = is.boolean(input) ? input : !this.playing;
|
||||||
|
|
||||||
if (toggle) {
|
if (toggle) {
|
||||||
this.play();
|
this.play();
|
||||||
@@ -399,7 +404,7 @@ class Plyr {
|
|||||||
if (this.isHTML5) {
|
if (this.isHTML5) {
|
||||||
this.pause();
|
this.pause();
|
||||||
this.restart();
|
this.restart();
|
||||||
} else if (utils.is.function(this.media.stop)) {
|
} else if (is.function(this.media.stop)) {
|
||||||
this.media.stop();
|
this.media.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,7 +421,7 @@ class Plyr {
|
|||||||
* @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
|
* @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
|
||||||
*/
|
*/
|
||||||
rewind(seekTime) {
|
rewind(seekTime) {
|
||||||
this.currentTime = this.currentTime - (utils.is.number(seekTime) ? seekTime : this.config.seekTime);
|
this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -424,7 +429,7 @@ class Plyr {
|
|||||||
* @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
|
* @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
|
||||||
*/
|
*/
|
||||||
forward(seekTime) {
|
forward(seekTime) {
|
||||||
this.currentTime = this.currentTime + (utils.is.number(seekTime) ? seekTime : this.config.seekTime);
|
this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -438,7 +443,7 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate input
|
// Validate input
|
||||||
const inputIsValid = utils.is.number(input) && input > 0;
|
const inputIsValid = is.number(input) && input > 0;
|
||||||
|
|
||||||
// Set
|
// Set
|
||||||
this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
|
this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
|
||||||
@@ -461,7 +466,7 @@ class Plyr {
|
|||||||
const { buffered } = this.media;
|
const { buffered } = this.media;
|
||||||
|
|
||||||
// YouTube / Vimeo return a float between 0-1
|
// YouTube / Vimeo return a float between 0-1
|
||||||
if (utils.is.number(buffered)) {
|
if (is.number(buffered)) {
|
||||||
return buffered;
|
return buffered;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,8 +494,9 @@ class Plyr {
|
|||||||
// Faux duration set via config
|
// Faux duration set via config
|
||||||
const fauxDuration = parseFloat(this.config.duration);
|
const fauxDuration = parseFloat(this.config.duration);
|
||||||
|
|
||||||
// Media duration can be NaN before the media has loaded
|
// Media duration can be NaN or Infinity before the media has loaded
|
||||||
const duration = (this.media || {}).duration || 0;
|
const realDuration = (this.media || {}).duration;
|
||||||
|
const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;
|
||||||
|
|
||||||
// If config duration is funky, use regular duration
|
// If config duration is funky, use regular duration
|
||||||
return fauxDuration || duration;
|
return fauxDuration || duration;
|
||||||
@@ -505,17 +511,17 @@ class Plyr {
|
|||||||
const max = 1;
|
const max = 1;
|
||||||
const min = 0;
|
const min = 0;
|
||||||
|
|
||||||
if (utils.is.string(volume)) {
|
if (is.string(volume)) {
|
||||||
volume = Number(volume);
|
volume = Number(volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load volume from storage if no value specified
|
// Load volume from storage if no value specified
|
||||||
if (!utils.is.number(volume)) {
|
if (!is.number(volume)) {
|
||||||
volume = this.storage.get('volume');
|
volume = this.storage.get('volume');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use config if all else fails
|
// Use config if all else fails
|
||||||
if (!utils.is.number(volume)) {
|
if (!is.number(volume)) {
|
||||||
({ volume } = this.config);
|
({ volume } = this.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,7 +541,7 @@ class Plyr {
|
|||||||
this.media.volume = volume;
|
this.media.volume = volume;
|
||||||
|
|
||||||
// If muted, and we're increasing volume manually, reset muted state
|
// If muted, and we're increasing volume manually, reset muted state
|
||||||
if (!utils.is.empty(value) && this.muted && volume > 0) {
|
if (!is.empty(value) && this.muted && volume > 0) {
|
||||||
this.muted = false;
|
this.muted = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -553,7 +559,7 @@ class Plyr {
|
|||||||
*/
|
*/
|
||||||
increaseVolume(step) {
|
increaseVolume(step) {
|
||||||
const volume = this.media.muted ? 0 : this.volume;
|
const volume = this.media.muted ? 0 : this.volume;
|
||||||
this.volume = volume + (utils.is.number(step) ? step : 1);
|
this.volume = volume + (is.number(step) ? step : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -561,8 +567,7 @@ class Plyr {
|
|||||||
* @param {boolean} step - How much to decrease by (between 0 and 1)
|
* @param {boolean} step - How much to decrease by (between 0 and 1)
|
||||||
*/
|
*/
|
||||||
decreaseVolume(step) {
|
decreaseVolume(step) {
|
||||||
const volume = this.media.muted ? 0 : this.volume;
|
this.increaseVolume(-step);
|
||||||
this.volume = volume - (utils.is.number(step) ? step : 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -573,12 +578,12 @@ class Plyr {
|
|||||||
let toggle = mute;
|
let toggle = mute;
|
||||||
|
|
||||||
// Load muted state from storage
|
// Load muted state from storage
|
||||||
if (!utils.is.boolean(toggle)) {
|
if (!is.boolean(toggle)) {
|
||||||
toggle = this.storage.get('muted');
|
toggle = this.storage.get('muted');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use config if all else fails
|
// Use config if all else fails
|
||||||
if (!utils.is.boolean(toggle)) {
|
if (!is.boolean(toggle)) {
|
||||||
toggle = this.config.muted;
|
toggle = this.config.muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,15 +629,15 @@ class Plyr {
|
|||||||
set speed(input) {
|
set speed(input) {
|
||||||
let speed = null;
|
let speed = null;
|
||||||
|
|
||||||
if (utils.is.number(input)) {
|
if (is.number(input)) {
|
||||||
speed = input;
|
speed = input;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!utils.is.number(speed)) {
|
if (!is.number(speed)) {
|
||||||
speed = this.storage.get('speed');
|
speed = this.storage.get('speed');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!utils.is.number(speed)) {
|
if (!is.number(speed)) {
|
||||||
speed = this.config.speed.selected;
|
speed = this.config.speed.selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,36 +674,31 @@ class Plyr {
|
|||||||
* @param {number} input - Quality level
|
* @param {number} input - Quality level
|
||||||
*/
|
*/
|
||||||
set quality(input) {
|
set quality(input) {
|
||||||
let quality = null;
|
const config = this.config.quality;
|
||||||
|
const options = this.options.quality;
|
||||||
|
|
||||||
if (!utils.is.empty(input)) {
|
if (!options.length) {
|
||||||
quality = Number(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!utils.is.number(quality)) {
|
|
||||||
quality = this.storage.get('quality');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!utils.is.number(quality)) {
|
|
||||||
quality = this.config.quality.selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!utils.is.number(quality)) {
|
|
||||||
quality = this.config.quality.default;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.options.quality.length) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.options.quality.includes(quality)) {
|
let quality = [
|
||||||
const closest = utils.closest(this.options.quality, quality);
|
!is.empty(input) && Number(input),
|
||||||
this.debug.warn(`Unsupported quality option: ${quality}, using ${closest} instead`);
|
this.storage.get('quality'),
|
||||||
quality = closest;
|
config.selected,
|
||||||
|
config.default,
|
||||||
|
].find(is.number);
|
||||||
|
|
||||||
|
if (!options.includes(quality)) {
|
||||||
|
const value = closest(options, quality);
|
||||||
|
this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);
|
||||||
|
quality = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger request event
|
||||||
|
triggerEvent.call(this, this.media, 'qualityrequested', false, { quality });
|
||||||
|
|
||||||
// Update config
|
// Update config
|
||||||
this.config.quality.selected = quality;
|
config.selected = quality;
|
||||||
|
|
||||||
// Set quality
|
// Set quality
|
||||||
this.media.quality = quality;
|
this.media.quality = quality;
|
||||||
@@ -717,7 +717,7 @@ class Plyr {
|
|||||||
* @param {boolean} input - Whether to loop or not
|
* @param {boolean} input - Whether to loop or not
|
||||||
*/
|
*/
|
||||||
set loop(input) {
|
set loop(input) {
|
||||||
const toggle = utils.is.boolean(input) ? input : this.config.loop.active;
|
const toggle = is.boolean(input) ? input : this.config.loop.active;
|
||||||
this.config.loop.active = toggle;
|
this.config.loop.active = toggle;
|
||||||
this.media.loop = toggle;
|
this.media.loop = toggle;
|
||||||
|
|
||||||
@@ -797,7 +797,7 @@ class Plyr {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.setPoster.call(this, input);
|
ui.setPoster.call(this, input, false).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -816,7 +816,7 @@ class Plyr {
|
|||||||
* @param {boolean} input - Whether to autoplay or not
|
* @param {boolean} input - Whether to autoplay or not
|
||||||
*/
|
*/
|
||||||
set autoplay(input) {
|
set autoplay(input) {
|
||||||
const toggle = utils.is.boolean(input) ? input : this.config.autoplay;
|
const toggle = is.boolean(input) ? input : this.config.autoplay;
|
||||||
this.config.autoplay = toggle;
|
this.config.autoplay = toggle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -832,25 +832,7 @@ class Plyr {
|
|||||||
* @param {boolean} input - Whether to enable captions
|
* @param {boolean} input - Whether to enable captions
|
||||||
*/
|
*/
|
||||||
toggleCaptions(input) {
|
toggleCaptions(input) {
|
||||||
// If there's no full support
|
captions.toggle.call(this, input, false);
|
||||||
if (!this.supported.ui) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the method is called without parameter, toggle based on current value
|
|
||||||
const active = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
|
|
||||||
|
|
||||||
// Toggle state
|
|
||||||
utils.toggleState(this.elements.buttons.captions, active);
|
|
||||||
|
|
||||||
// Add class hook
|
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, active);
|
|
||||||
|
|
||||||
// Update state and trigger event
|
|
||||||
if (active !== this.captions.active) {
|
|
||||||
this.captions.active = active;
|
|
||||||
utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -858,15 +840,15 @@ class Plyr {
|
|||||||
* @param {number} - Caption index
|
* @param {number} - Caption index
|
||||||
*/
|
*/
|
||||||
set currentTrack(input) {
|
set currentTrack(input) {
|
||||||
captions.set.call(this, input);
|
captions.set.call(this, input, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current caption track index (-1 if disabled)
|
* Get the current caption track index (-1 if disabled)
|
||||||
*/
|
*/
|
||||||
get currentTrack() {
|
get currentTrack() {
|
||||||
const { active, currentTrack } = this.captions;
|
const { toggled, currentTrack } = this.captions;
|
||||||
return active ? currentTrack : -1;
|
return toggled ? currentTrack : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -875,7 +857,7 @@ class Plyr {
|
|||||||
* @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
|
* @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
|
||||||
*/
|
*/
|
||||||
set language(input) {
|
set language(input) {
|
||||||
captions.setLanguage.call(this, input);
|
captions.setLanguage.call(this, input, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -902,7 +884,7 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Toggle based on current state if not passed
|
// Toggle based on current state if not passed
|
||||||
const toggle = utils.is.boolean(input) ? input : this.pip === states.inline;
|
const toggle = is.boolean(input) ? input : this.pip === states.inline;
|
||||||
|
|
||||||
// Toggle based on current state
|
// Toggle based on current state
|
||||||
this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline);
|
this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline);
|
||||||
@@ -938,22 +920,22 @@ class Plyr {
|
|||||||
// Don't toggle if missing UI support or if it's audio
|
// Don't toggle if missing UI support or if it's audio
|
||||||
if (this.supported.ui && !this.isAudio) {
|
if (this.supported.ui && !this.isAudio) {
|
||||||
// Get state before change
|
// Get state before change
|
||||||
const isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
|
const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);
|
||||||
|
|
||||||
// Negate the argument if not undefined since adding the class to hides the controls
|
// Negate the argument if not undefined since adding the class to hides the controls
|
||||||
const force = typeof toggle === 'undefined' ? undefined : !toggle;
|
const force = typeof toggle === 'undefined' ? undefined : !toggle;
|
||||||
|
|
||||||
// Apply and get updated state
|
// Apply and get updated state
|
||||||
const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force);
|
const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);
|
||||||
|
|
||||||
// Close menu
|
// Close menu
|
||||||
if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
|
if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
|
||||||
controls.toggleMenu.call(this, false);
|
controls.toggleMenu.call(this, false);
|
||||||
}
|
}
|
||||||
// Trigger event on change
|
// Trigger event on change
|
||||||
if (hiding !== isHidden) {
|
if (hiding !== isHidden) {
|
||||||
const eventName = hiding ? 'controlshidden' : 'controlsshown';
|
const eventName = hiding ? 'controlshidden' : 'controlsshown';
|
||||||
utils.dispatchEvent.call(this, this.media, eventName);
|
triggerEvent.call(this, this.media, eventName);
|
||||||
}
|
}
|
||||||
return !hiding;
|
return !hiding;
|
||||||
}
|
}
|
||||||
@@ -966,7 +948,16 @@ class Plyr {
|
|||||||
* @param {function} callback - Callback for when event occurs
|
* @param {function} callback - Callback for when event occurs
|
||||||
*/
|
*/
|
||||||
on(event, callback) {
|
on(event, callback) {
|
||||||
utils.on(this.elements.container, event, callback);
|
on.call(this, this.elements.container, event, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add event listeners once
|
||||||
|
* @param {string} event - Event type
|
||||||
|
* @param {function} callback - Callback for when event occurs
|
||||||
|
*/
|
||||||
|
once(event, callback) {
|
||||||
|
once.call(this, this.elements.container, event, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -975,7 +966,7 @@ class Plyr {
|
|||||||
* @param {function} callback - Callback for when event occurs
|
* @param {function} callback - Callback for when event occurs
|
||||||
*/
|
*/
|
||||||
off(event, callback) {
|
off(event, callback) {
|
||||||
utils.off(this.elements.container, event, callback);
|
off(this.elements.container, event, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1001,10 +992,10 @@ class Plyr {
|
|||||||
if (soft) {
|
if (soft) {
|
||||||
if (Object.keys(this.elements).length) {
|
if (Object.keys(this.elements).length) {
|
||||||
// Remove elements
|
// Remove elements
|
||||||
utils.removeElement(this.elements.buttons.play);
|
removeElement(this.elements.buttons.play);
|
||||||
utils.removeElement(this.elements.captions);
|
removeElement(this.elements.captions);
|
||||||
utils.removeElement(this.elements.controls);
|
removeElement(this.elements.controls);
|
||||||
utils.removeElement(this.elements.wrapper);
|
removeElement(this.elements.wrapper);
|
||||||
|
|
||||||
// Clear for GC
|
// Clear for GC
|
||||||
this.elements.buttons.play = null;
|
this.elements.buttons.play = null;
|
||||||
@@ -1014,21 +1005,21 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Callback
|
// Callback
|
||||||
if (utils.is.function(callback)) {
|
if (is.function(callback)) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unbind listeners
|
// Unbind listeners
|
||||||
this.listeners.clear();
|
unbindListeners.call(this);
|
||||||
|
|
||||||
// Replace the container with the original element provided
|
// Replace the container with the original element provided
|
||||||
utils.replaceElement(this.elements.original, this.elements.container);
|
replaceElement(this.elements.original, this.elements.container);
|
||||||
|
|
||||||
// Event
|
// Event
|
||||||
utils.dispatchEvent.call(this, this.elements.original, 'destroyed', true);
|
triggerEvent.call(this, this.elements.original, 'destroyed', true);
|
||||||
|
|
||||||
// Callback
|
// Callback
|
||||||
if (utils.is.function(callback)) {
|
if (is.function(callback)) {
|
||||||
callback.call(this.elements.original);
|
callback.call(this.elements.original);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1046,50 +1037,37 @@ class Plyr {
|
|||||||
// Stop playback
|
// Stop playback
|
||||||
this.stop();
|
this.stop();
|
||||||
|
|
||||||
// Type specific stuff
|
// Provider specific stuff
|
||||||
switch (`${this.provider}:${this.type}`) {
|
if (this.isHTML5) {
|
||||||
case 'html5:video':
|
// Clear timeout
|
||||||
case 'html5:audio':
|
clearTimeout(this.timers.loading);
|
||||||
// Clear timeout
|
|
||||||
clearTimeout(this.timers.loading);
|
|
||||||
|
|
||||||
// Restore native video controls
|
// Restore native video controls
|
||||||
ui.toggleNativeControls.call(this, true);
|
ui.toggleNativeControls.call(this, true);
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
done();
|
done();
|
||||||
|
} else if (this.isYouTube) {
|
||||||
|
// Clear timers
|
||||||
|
clearInterval(this.timers.buffering);
|
||||||
|
clearInterval(this.timers.playing);
|
||||||
|
|
||||||
break;
|
// Destroy YouTube API
|
||||||
|
if (this.embed !== null && is.function(this.embed.destroy)) {
|
||||||
|
this.embed.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
case 'youtube:video':
|
// Clean up
|
||||||
// Clear timers
|
done();
|
||||||
clearInterval(this.timers.buffering);
|
} else if (this.isVimeo) {
|
||||||
clearInterval(this.timers.playing);
|
// Destroy Vimeo API
|
||||||
|
// then clean up (wait, to prevent postmessage errors)
|
||||||
|
if (this.embed !== null) {
|
||||||
|
this.embed.unload().then(done);
|
||||||
|
}
|
||||||
|
|
||||||
// Destroy YouTube API
|
// Vimeo does not always return
|
||||||
if (this.embed !== null && utils.is.function(this.embed.destroy)) {
|
setTimeout(done, 200);
|
||||||
this.embed.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
done();
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'vimeo:video':
|
|
||||||
// Destroy Vimeo API
|
|
||||||
// then clean up (wait, to prevent postmessage errors)
|
|
||||||
if (this.embed !== null) {
|
|
||||||
this.embed.unload().then(done);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vimeo does not always return
|
|
||||||
setTimeout(done, 200);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,7 +1095,7 @@ class Plyr {
|
|||||||
* @param {string} [id] - Unique ID
|
* @param {string} [id] - Unique ID
|
||||||
*/
|
*/
|
||||||
static loadSprite(url, id) {
|
static loadSprite(url, id) {
|
||||||
return utils.loadSprite(url, id);
|
return loadSprite(url, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1128,15 +1106,15 @@ class Plyr {
|
|||||||
static setup(selector, options = {}) {
|
static setup(selector, options = {}) {
|
||||||
let targets = null;
|
let targets = null;
|
||||||
|
|
||||||
if (utils.is.string(selector)) {
|
if (is.string(selector)) {
|
||||||
targets = Array.from(document.querySelectorAll(selector));
|
targets = Array.from(document.querySelectorAll(selector));
|
||||||
} else if (utils.is.nodeList(selector)) {
|
} else if (is.nodeList(selector)) {
|
||||||
targets = Array.from(selector);
|
targets = Array.from(selector);
|
||||||
} else if (utils.is.array(selector)) {
|
} else if (is.array(selector)) {
|
||||||
targets = selector.filter(utils.is.element);
|
targets = selector.filter(is.element);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (utils.is.empty(targets)) {
|
if (is.empty(targets)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,6 +1122,6 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Plyr.defaults = utils.cloneDeep(defaults);
|
Plyr.defaults = cloneDeep(defaults);
|
||||||
|
|
||||||
export default Plyr;
|
export default Plyr;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Plyr Polyfilled Build
|
// Plyr Polyfilled Build
|
||||||
// plyr.js v3.3.12
|
// plyr.js v3.3.22
|
||||||
// https://github.com/sampotts/plyr
|
// https://github.com/sampotts/plyr
|
||||||
// License: The MIT License (MIT)
|
// License: The MIT License (MIT)
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|||||||
+26
-37
@@ -2,23 +2,25 @@
|
|||||||
// Plyr source update
|
// Plyr source update
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
|
import { providers } from './config/types';
|
||||||
import html5 from './html5';
|
import html5 from './html5';
|
||||||
import media from './media';
|
import media from './media';
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import { providers } from './types';
|
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
import utils from './utils';
|
import { createElement, insertElement, removeElement } from './utils/elements';
|
||||||
|
import is from './utils/is';
|
||||||
|
import { getDeep } from './utils/objects';
|
||||||
|
|
||||||
const source = {
|
const source = {
|
||||||
// Add elements to HTML5 media (source, tracks, etc)
|
// Add elements to HTML5 media (source, tracks, etc)
|
||||||
insertElements(type, attributes) {
|
insertElements(type, attributes) {
|
||||||
if (utils.is.string(attributes)) {
|
if (is.string(attributes)) {
|
||||||
utils.insertElement(type, this.media, {
|
insertElement(type, this.media, {
|
||||||
src: attributes,
|
src: attributes,
|
||||||
});
|
});
|
||||||
} else if (utils.is.array(attributes)) {
|
} else if (is.array(attributes)) {
|
||||||
attributes.forEach(attribute => {
|
attributes.forEach(attribute => {
|
||||||
utils.insertElement(type, this.media, attribute);
|
insertElement(type, this.media, attribute);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -26,7 +28,7 @@ const source = {
|
|||||||
// Update source
|
// Update source
|
||||||
// Sources are not checked for support so be careful
|
// Sources are not checked for support so be careful
|
||||||
change(input) {
|
change(input) {
|
||||||
if (!utils.is.object(input) || !('sources' in input) || !input.sources.length) {
|
if (!getDeep(input, 'sources.length')) {
|
||||||
this.debug.warn('Invalid source format');
|
this.debug.warn('Invalid source format');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -42,47 +44,34 @@ const source = {
|
|||||||
this.options.quality = [];
|
this.options.quality = [];
|
||||||
|
|
||||||
// Remove elements
|
// Remove elements
|
||||||
utils.removeElement(this.media);
|
removeElement(this.media);
|
||||||
this.media = null;
|
this.media = null;
|
||||||
|
|
||||||
// Reset class name
|
// Reset class name
|
||||||
if (utils.is.element(this.elements.container)) {
|
if (is.element(this.elements.container)) {
|
||||||
this.elements.container.removeAttribute('class');
|
this.elements.container.removeAttribute('class');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the type and provider
|
// Set the type and provider
|
||||||
this.type = input.type;
|
const { sources, type } = input;
|
||||||
this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5;
|
const [{ provider = providers.html5, src }] = sources;
|
||||||
|
const tagName = provider === 'html5' ? type : 'div';
|
||||||
|
const attributes = provider === 'html5' ? {} : { src };
|
||||||
|
|
||||||
// Check for support
|
Object.assign(this, {
|
||||||
this.supported = support.check(this.type, this.provider, this.config.playsinline);
|
provider,
|
||||||
|
type,
|
||||||
// Create new markup
|
// Check for support
|
||||||
switch (`${this.provider}:${this.type}`) {
|
supported: support.check(type, provider, this.config.playsinline),
|
||||||
case 'html5:video':
|
// Create new element
|
||||||
this.media = utils.createElement('video');
|
media: createElement(tagName, attributes),
|
||||||
break;
|
});
|
||||||
|
|
||||||
case 'html5:audio':
|
|
||||||
this.media = utils.createElement('audio');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'youtube:video':
|
|
||||||
case 'vimeo:video':
|
|
||||||
this.media = utils.createElement('div', {
|
|
||||||
src: input.sources[0].src,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject the new element
|
// Inject the new element
|
||||||
this.elements.container.appendChild(this.media);
|
this.elements.container.appendChild(this.media);
|
||||||
|
|
||||||
// Autoplay the new source?
|
// Autoplay the new source?
|
||||||
if (utils.is.boolean(input.autoplay)) {
|
if (is.boolean(input.autoplay)) {
|
||||||
this.config.autoplay = input.autoplay;
|
this.config.autoplay = input.autoplay;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +83,7 @@ const source = {
|
|||||||
if (this.config.autoplay) {
|
if (this.config.autoplay) {
|
||||||
this.media.setAttribute('autoplay', '');
|
this.media.setAttribute('autoplay', '');
|
||||||
}
|
}
|
||||||
if (!utils.is.empty(input.poster)) {
|
if (!is.empty(input.poster)) {
|
||||||
this.poster = input.poster;
|
this.poster = input.poster;
|
||||||
}
|
}
|
||||||
if (this.config.loop.active) {
|
if (this.config.loop.active) {
|
||||||
@@ -113,7 +102,7 @@ const source = {
|
|||||||
|
|
||||||
// Set new sources for html5
|
// Set new sources for html5
|
||||||
if (this.isHTML5) {
|
if (this.isHTML5) {
|
||||||
source.insertElements.call(this, 'source', input.sources);
|
source.insertElements.call(this, 'source', sources);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set video title
|
// Set video title
|
||||||
|
|||||||
+7
-6
@@ -2,7 +2,8 @@
|
|||||||
// Plyr storage
|
// Plyr storage
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import utils from './utils';
|
import is from './utils/is';
|
||||||
|
import { extend } from './utils/objects';
|
||||||
|
|
||||||
class Storage {
|
class Storage {
|
||||||
constructor(player) {
|
constructor(player) {
|
||||||
@@ -37,13 +38,13 @@ class Storage {
|
|||||||
|
|
||||||
const store = window.localStorage.getItem(this.key);
|
const store = window.localStorage.getItem(this.key);
|
||||||
|
|
||||||
if (utils.is.empty(store)) {
|
if (is.empty(store)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = JSON.parse(store);
|
const json = JSON.parse(store);
|
||||||
|
|
||||||
return utils.is.string(key) && key.length ? json[key] : json;
|
return is.string(key) && key.length ? json[key] : json;
|
||||||
}
|
}
|
||||||
|
|
||||||
set(object) {
|
set(object) {
|
||||||
@@ -53,7 +54,7 @@ class Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Can only store objectst
|
// Can only store objectst
|
||||||
if (!utils.is.object(object)) {
|
if (!is.object(object)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,12 +62,12 @@ class Storage {
|
|||||||
let storage = this.get();
|
let storage = this.get();
|
||||||
|
|
||||||
// Default to empty object
|
// Default to empty object
|
||||||
if (utils.is.empty(storage)) {
|
if (is.empty(storage)) {
|
||||||
storage = {};
|
storage = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the working copy of the values
|
// Update the working copy of the values
|
||||||
utils.extend(storage, object);
|
extend(storage, object);
|
||||||
|
|
||||||
// Update storage
|
// Update storage
|
||||||
window.localStorage.setItem(this.key, JSON.stringify(storage));
|
window.localStorage.setItem(this.key, JSON.stringify(storage));
|
||||||
|
|||||||
+38
-101
@@ -2,7 +2,19 @@
|
|||||||
// Plyr support checks
|
// Plyr support checks
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import utils from './utils';
|
import { transitionEndEvent } from './utils/animation';
|
||||||
|
import browser from './utils/browser';
|
||||||
|
import { createElement } from './utils/elements';
|
||||||
|
import is from './utils/is';
|
||||||
|
|
||||||
|
// Default codecs for checking mimetype support
|
||||||
|
const defaultCodecs = {
|
||||||
|
'audio/ogg': 'vorbis',
|
||||||
|
'audio/wav': '1',
|
||||||
|
'video/webm': 'vp8, vorbis',
|
||||||
|
'video/mp4': 'avc1.42E01E, mp4a.40.2',
|
||||||
|
'video/ogg': 'theora',
|
||||||
|
};
|
||||||
|
|
||||||
// Check for feature support
|
// Check for feature support
|
||||||
const support = {
|
const support = {
|
||||||
@@ -13,32 +25,9 @@ const support = {
|
|||||||
// Check for support
|
// Check for support
|
||||||
// Basic functionality vs full UI
|
// Basic functionality vs full UI
|
||||||
check(type, provider, playsinline) {
|
check(type, provider, playsinline) {
|
||||||
let api = false;
|
|
||||||
let ui = false;
|
|
||||||
const browser = utils.getBrowser();
|
|
||||||
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
|
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
|
||||||
|
const api = support[type] || provider !== 'html5';
|
||||||
switch (`${provider}:${type}`) {
|
const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
|
||||||
case 'html5:video':
|
|
||||||
api = support.video;
|
|
||||||
ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'html5:audio':
|
|
||||||
api = support.audio;
|
|
||||||
ui = api && support.rangeInput;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'youtube:video':
|
|
||||||
case 'vimeo:video':
|
|
||||||
api = true;
|
|
||||||
ui = support.rangeInput && (!browser.isIPhone || canPlayInline);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
api = support.audio && support.video;
|
|
||||||
ui = api && support.rangeInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
api,
|
api,
|
||||||
@@ -48,14 +37,11 @@ const support = {
|
|||||||
|
|
||||||
// Picture-in-picture support
|
// Picture-in-picture support
|
||||||
// Safari only currently
|
// Safari only currently
|
||||||
pip: (() => {
|
pip: (() => !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode))(),
|
||||||
const browser = utils.getBrowser();
|
|
||||||
return !browser.isIPhone && utils.is.function(utils.createElement('video').webkitSetPresentationMode);
|
|
||||||
})(),
|
|
||||||
|
|
||||||
// Airplay support
|
// Airplay support
|
||||||
// Safari only currently
|
// Safari only currently
|
||||||
airplay: utils.is.function(window.WebKitPlaybackTargetAvailabilityEvent),
|
airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
|
||||||
|
|
||||||
// Inline playback support
|
// Inline playback support
|
||||||
// https://webkit.org/blog/6784/new-video-policies-for-ios/
|
// https://webkit.org/blog/6784/new-video-policies-for-ios/
|
||||||
@@ -64,83 +50,34 @@ const support = {
|
|||||||
// Check for mime type support against a player instance
|
// Check for mime type support against a player instance
|
||||||
// Credits: http://diveintohtml5.info/everything.html
|
// Credits: http://diveintohtml5.info/everything.html
|
||||||
// Related: http://www.leanbackplayer.com/test/h5mt.html
|
// Related: http://www.leanbackplayer.com/test/h5mt.html
|
||||||
mime(type) {
|
mime(inputType) {
|
||||||
const { media } = this;
|
const [mediaType] = inputType.split('/');
|
||||||
|
if (!this.isHTML5 || mediaType !== this.type) {
|
||||||
try {
|
|
||||||
// Bail if no checking function
|
|
||||||
if (!this.isHTML5 || !utils.is.function(media.canPlayType)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check directly if codecs specified
|
|
||||||
if (type.includes('codecs=')) {
|
|
||||||
return media.canPlayType(type).replace(/no/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type specific checks
|
|
||||||
if (this.isVideo) {
|
|
||||||
switch (type) {
|
|
||||||
case 'video/webm':
|
|
||||||
return media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, '');
|
|
||||||
|
|
||||||
case 'video/mp4':
|
|
||||||
return media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, '');
|
|
||||||
|
|
||||||
case 'video/ogg':
|
|
||||||
return media.canPlayType('video/ogg; codecs="theora"').replace(/no/, '');
|
|
||||||
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (this.isAudio) {
|
|
||||||
switch (type) {
|
|
||||||
case 'audio/mpeg':
|
|
||||||
return media.canPlayType('audio/mpeg;').replace(/no/, '');
|
|
||||||
|
|
||||||
case 'audio/ogg':
|
|
||||||
return media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, '');
|
|
||||||
|
|
||||||
case 'audio/wav':
|
|
||||||
return media.canPlayType('audio/wav; codecs="1"').replace(/no/, '');
|
|
||||||
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we got this far, we're stuffed
|
let type;
|
||||||
return false;
|
if (inputType && inputType.includes('codecs=')) {
|
||||||
|
// Use input directly
|
||||||
|
type = inputType;
|
||||||
|
} else if (inputType === 'audio/mpeg') {
|
||||||
|
// Skip codec
|
||||||
|
type = 'audio/mpeg;';
|
||||||
|
} else if (inputType in defaultCodecs) {
|
||||||
|
// Use codec
|
||||||
|
type = `${inputType}; codecs="${defaultCodecs[inputType]}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check for textTracks support
|
// Check for textTracks support
|
||||||
textTracks: 'textTracks' in document.createElement('video'),
|
textTracks: 'textTracks' in document.createElement('video'),
|
||||||
|
|
||||||
// Check for passive event listener support
|
|
||||||
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
|
|
||||||
// https://www.youtube.com/watch?v=NPM6172J22g
|
|
||||||
passiveListeners: (() => {
|
|
||||||
// Test via a getter in the options object to see if the passive property is accessed
|
|
||||||
let supported = false;
|
|
||||||
try {
|
|
||||||
const options = Object.defineProperty({}, 'passive', {
|
|
||||||
get() {
|
|
||||||
supported = true;
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
window.addEventListener('test', null, options);
|
|
||||||
window.removeEventListener('test', null, options);
|
|
||||||
} catch (e) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
return supported;
|
|
||||||
})(),
|
|
||||||
|
|
||||||
// <input type="range"> Sliders
|
// <input type="range"> Sliders
|
||||||
rangeInput: (() => {
|
rangeInput: (() => {
|
||||||
const range = document.createElement('input');
|
const range = document.createElement('input');
|
||||||
@@ -153,7 +90,7 @@ const support = {
|
|||||||
touch: 'ontouchstart' in document.documentElement,
|
touch: 'ontouchstart' in document.documentElement,
|
||||||
|
|
||||||
// Detect transitions support
|
// Detect transitions support
|
||||||
transitions: utils.transitionEndEvent !== false,
|
transitions: transitionEndEvent !== false,
|
||||||
|
|
||||||
// Reduced motion iOS & MacOS setting
|
// Reduced motion iOS & MacOS setting
|
||||||
// https://webkit.org/blog/7551/responsive-design-for-motion/
|
// https://webkit.org/blog/7551/responsive-design-for-motion/
|
||||||
|
|||||||
+74
-62
@@ -6,15 +6,16 @@ import captions from './captions';
|
|||||||
import controls from './controls';
|
import controls from './controls';
|
||||||
import i18n from './i18n';
|
import i18n from './i18n';
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import utils from './utils';
|
import browser from './utils/browser';
|
||||||
|
import { getElement, toggleClass } from './utils/elements';
|
||||||
// Sniff out the browser
|
import { ready, triggerEvent } from './utils/events';
|
||||||
const browser = utils.getBrowser();
|
import is from './utils/is';
|
||||||
|
import loadImage from './utils/loadImage';
|
||||||
|
|
||||||
const ui = {
|
const ui = {
|
||||||
addStyleHook() {
|
addStyleHook() {
|
||||||
utils.toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
|
toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
|
toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Toggle native HTML5 media controls
|
// Toggle native HTML5 media controls
|
||||||
@@ -44,7 +45,7 @@ const ui = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inject custom controls if not present
|
// Inject custom controls if not present
|
||||||
if (!utils.is.element(this.elements.controls)) {
|
if (!is.element(this.elements.controls)) {
|
||||||
// Inject custom controls
|
// Inject custom controls
|
||||||
controls.inject.call(this);
|
controls.inject.call(this);
|
||||||
|
|
||||||
@@ -85,31 +86,35 @@ const ui = {
|
|||||||
ui.checkPlaying.call(this);
|
ui.checkPlaying.call(this);
|
||||||
|
|
||||||
// Check for picture-in-picture support
|
// Check for picture-in-picture support
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
|
toggleClass(
|
||||||
|
this.elements.container,
|
||||||
|
this.config.classNames.pip.supported,
|
||||||
|
support.pip && this.isHTML5 && this.isVideo,
|
||||||
|
);
|
||||||
|
|
||||||
// Check for airplay support
|
// Check for airplay support
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
||||||
|
|
||||||
// Add iOS class
|
// Add iOS class
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
||||||
|
|
||||||
// Add touch class
|
// Add touch class
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
|
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
|
||||||
|
|
||||||
// Ready for API calls
|
// Ready for API calls
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
|
|
||||||
// Ready event at end of execution stack
|
// Ready event at end of execution stack
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
utils.dispatchEvent.call(this, this.media, 'ready');
|
triggerEvent.call(this, this.media, 'ready');
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// Set the title
|
// Set the title
|
||||||
ui.setTitle.call(this);
|
ui.setTitle.call(this);
|
||||||
|
|
||||||
// Assure the poster image is set, if the property was added before the element was created
|
// Assure the poster image is set, if the property was added before the element was created
|
||||||
if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) {
|
if (this.poster) {
|
||||||
ui.setPoster.call(this, this.poster);
|
ui.setPoster.call(this, this.poster, false).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manually set the duration if user has overridden it.
|
// Manually set the duration if user has overridden it.
|
||||||
@@ -125,31 +130,26 @@ const ui = {
|
|||||||
let label = i18n.get('play', this.config);
|
let label = i18n.get('play', this.config);
|
||||||
|
|
||||||
// If there's a media title set, use that for the label
|
// If there's a media title set, use that for the label
|
||||||
if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) {
|
if (is.string(this.config.title) && !is.empty(this.config.title)) {
|
||||||
label += `, ${this.config.title}`;
|
label += `, ${this.config.title}`;
|
||||||
|
|
||||||
// Set container label
|
|
||||||
this.elements.container.setAttribute('aria-label', this.config.title);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's a play button, set label
|
// If there's a play button, set label
|
||||||
if (utils.is.nodeList(this.elements.buttons.play)) {
|
Array.from(this.elements.buttons.play || []).forEach(button => {
|
||||||
Array.from(this.elements.buttons.play).forEach(button => {
|
button.setAttribute('aria-label', label);
|
||||||
button.setAttribute('aria-label', label);
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set iframe title
|
// Set iframe title
|
||||||
// https://github.com/sampotts/plyr/issues/124
|
// https://github.com/sampotts/plyr/issues/124
|
||||||
if (this.isEmbed) {
|
if (this.isEmbed) {
|
||||||
const iframe = utils.getElement.call(this, 'iframe');
|
const iframe = getElement.call(this, 'iframe');
|
||||||
|
|
||||||
if (!utils.is.element(iframe)) {
|
if (!is.element(iframe)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to media type
|
// Default to media type
|
||||||
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
|
const title = !is.empty(this.config.title) ? this.config.title : 'video';
|
||||||
const format = i18n.get('frameTitle', this.config);
|
const format = i18n.get('frameTitle', this.config);
|
||||||
|
|
||||||
iframe.setAttribute('title', format.replace('{title}', title));
|
iframe.setAttribute('title', format.replace('{title}', title));
|
||||||
@@ -158,51 +158,66 @@ const ui = {
|
|||||||
|
|
||||||
// Toggle poster
|
// Toggle poster
|
||||||
togglePoster(enable) {
|
togglePoster(enable) {
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
|
toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Set the poster image (async)
|
// Set the poster image (async)
|
||||||
setPoster(poster) {
|
// Used internally for the poster setter, with the passive option forced to false
|
||||||
// Set property regardless of validity
|
setPoster(poster, passive = true) {
|
||||||
this.media.setAttribute('poster', poster);
|
// Don't override if call is passive
|
||||||
|
if (passive && this.poster) {
|
||||||
// Bail if element is missing
|
return Promise.reject(new Error('Poster already set'));
|
||||||
if (!utils.is.element(this.elements.poster)) {
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the image, and set poster if successful
|
// Set property synchronously to respect the call order
|
||||||
const loadPromise = utils.loadImage(poster)
|
this.media.setAttribute('poster', poster);
|
||||||
.then(() => {
|
|
||||||
this.elements.poster.style.backgroundImage = `url('${poster}')`;
|
|
||||||
Object.assign(this.elements.poster.style, {
|
|
||||||
backgroundImage: `url('${poster}')`,
|
|
||||||
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
|
|
||||||
backgroundSize: '',
|
|
||||||
});
|
|
||||||
ui.togglePoster.call(this, true);
|
|
||||||
return poster;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video)
|
// Wait until ui is ready
|
||||||
loadPromise.catch(() => ui.togglePoster.call(this, false));
|
return (
|
||||||
|
ready
|
||||||
// Return the promise so the caller can use it as well
|
.call(this)
|
||||||
return loadPromise;
|
// Load image
|
||||||
|
.then(() => loadImage(poster))
|
||||||
|
.catch(err => {
|
||||||
|
// Hide poster on error unless it's been set by another call
|
||||||
|
if (poster === this.poster) {
|
||||||
|
ui.togglePoster.call(this, false);
|
||||||
|
}
|
||||||
|
// Rethrow
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// Prevent race conditions
|
||||||
|
if (poster !== this.poster) {
|
||||||
|
throw new Error('setPoster cancelled by later call to setPoster');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
Object.assign(this.elements.poster.style, {
|
||||||
|
backgroundImage: `url('${poster}')`,
|
||||||
|
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
|
||||||
|
backgroundSize: '',
|
||||||
|
});
|
||||||
|
ui.togglePoster.call(this, true);
|
||||||
|
return poster;
|
||||||
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check playing state
|
// Check playing state
|
||||||
checkPlaying(event) {
|
checkPlaying(event) {
|
||||||
// Class hooks
|
// Class hooks
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
|
toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
|
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
|
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
|
||||||
|
|
||||||
// Set ARIA state
|
// Set state
|
||||||
utils.toggleState(this.elements.buttons.play, this.playing);
|
Array.from(this.elements.buttons.play || []).forEach(target => {
|
||||||
|
target.pressed = this.playing;
|
||||||
|
});
|
||||||
|
|
||||||
// Only update controls on non timeupdate events
|
// Only update controls on non timeupdate events
|
||||||
if (utils.is.event(event) && event.type === 'timeupdate') {
|
if (is.event(event) && event.type === 'timeupdate') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,10 +227,7 @@ const ui = {
|
|||||||
|
|
||||||
// Check if media is loading
|
// Check if media is loading
|
||||||
checkLoading(event) {
|
checkLoading(event) {
|
||||||
this.loading = [
|
this.loading = ['stalled', 'waiting'].includes(event.type);
|
||||||
'stalled',
|
|
||||||
'waiting',
|
|
||||||
].includes(event.type);
|
|
||||||
|
|
||||||
// Clear timer
|
// Clear timer
|
||||||
clearTimeout(this.timers.loading);
|
clearTimeout(this.timers.loading);
|
||||||
@@ -223,7 +235,7 @@ const ui = {
|
|||||||
// Timer to prevent flicker when seeking
|
// Timer to prevent flicker when seeking
|
||||||
this.timers.loading = setTimeout(() => {
|
this.timers.loading = setTimeout(() => {
|
||||||
// Update progress bar loading class state
|
// Update progress bar loading class state
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||||
|
|
||||||
// Update controls visibility
|
// Update controls visibility
|
||||||
ui.toggleControls.call(this);
|
ui.toggleControls.call(this);
|
||||||
|
|||||||
-875
@@ -1,875 +0,0 @@
|
|||||||
// ==========================================================================
|
|
||||||
// Plyr utils
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
import loadjs from 'loadjs';
|
|
||||||
import Storage from './storage';
|
|
||||||
import support from './support';
|
|
||||||
import { providers } from './types';
|
|
||||||
|
|
||||||
const utils = {
|
|
||||||
// Check variable types
|
|
||||||
is: {
|
|
||||||
object(input) {
|
|
||||||
return utils.getConstructor(input) === Object;
|
|
||||||
},
|
|
||||||
number(input) {
|
|
||||||
return utils.getConstructor(input) === Number && !Number.isNaN(input);
|
|
||||||
},
|
|
||||||
string(input) {
|
|
||||||
return utils.getConstructor(input) === String;
|
|
||||||
},
|
|
||||||
boolean(input) {
|
|
||||||
return utils.getConstructor(input) === Boolean;
|
|
||||||
},
|
|
||||||
function(input) {
|
|
||||||
return utils.getConstructor(input) === Function;
|
|
||||||
},
|
|
||||||
array(input) {
|
|
||||||
return !utils.is.nullOrUndefined(input) && Array.isArray(input);
|
|
||||||
},
|
|
||||||
weakMap(input) {
|
|
||||||
return utils.is.instanceof(input, WeakMap);
|
|
||||||
},
|
|
||||||
nodeList(input) {
|
|
||||||
return utils.is.instanceof(input, NodeList);
|
|
||||||
},
|
|
||||||
element(input) {
|
|
||||||
return utils.is.instanceof(input, Element);
|
|
||||||
},
|
|
||||||
textNode(input) {
|
|
||||||
return utils.getConstructor(input) === Text;
|
|
||||||
},
|
|
||||||
event(input) {
|
|
||||||
return utils.is.instanceof(input, Event);
|
|
||||||
},
|
|
||||||
cue(input) {
|
|
||||||
return utils.is.instanceof(input, window.TextTrackCue) || utils.is.instanceof(input, window.VTTCue);
|
|
||||||
},
|
|
||||||
track(input) {
|
|
||||||
return utils.is.instanceof(input, TextTrack) || (!utils.is.nullOrUndefined(input) && utils.is.string(input.kind));
|
|
||||||
},
|
|
||||||
url(input) {
|
|
||||||
return !utils.is.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input);
|
|
||||||
},
|
|
||||||
nullOrUndefined(input) {
|
|
||||||
return input === null || typeof input === 'undefined';
|
|
||||||
},
|
|
||||||
empty(input) {
|
|
||||||
return (
|
|
||||||
utils.is.nullOrUndefined(input) ||
|
|
||||||
((utils.is.string(input) || utils.is.array(input) || utils.is.nodeList(input)) && !input.length) ||
|
|
||||||
(utils.is.object(input) && !Object.keys(input).length)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
instanceof(input, constructor) {
|
|
||||||
return Boolean(input && constructor && input instanceof constructor);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
getConstructor(input) {
|
|
||||||
return !utils.is.nullOrUndefined(input) ? input.constructor : null;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Unfortunately, due to mixed support, UA sniffing is required
|
|
||||||
getBrowser() {
|
|
||||||
return {
|
|
||||||
isIE: /* @cc_on!@ */ false || !!document.documentMode,
|
|
||||||
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
|
|
||||||
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
|
|
||||||
isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// Fetch wrapper
|
|
||||||
// Using XHR to avoid issues with older browsers
|
|
||||||
fetch(url, responseType = 'text') {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
const request = new XMLHttpRequest();
|
|
||||||
|
|
||||||
// Check for CORS support
|
|
||||||
if (!('withCredentials' in request)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
request.addEventListener('load', () => {
|
|
||||||
if (responseType === 'text') {
|
|
||||||
try {
|
|
||||||
resolve(JSON.parse(request.responseText));
|
|
||||||
} catch (e) {
|
|
||||||
resolve(request.responseText);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolve(request.response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
request.addEventListener('error', () => {
|
|
||||||
throw new Error(request.statusText);
|
|
||||||
});
|
|
||||||
|
|
||||||
request.open('GET', url, true);
|
|
||||||
|
|
||||||
// Set the required response type
|
|
||||||
request.responseType = responseType;
|
|
||||||
|
|
||||||
request.send();
|
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Load image avoiding xhr/fetch CORS issues
|
|
||||||
// Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded.
|
|
||||||
// By default it checks if it is at least 1px, but you can add a second argument to change this.
|
|
||||||
loadImage(src, minWidth = 1) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const image = new Image();
|
|
||||||
const handler = () => {
|
|
||||||
delete image.onload;
|
|
||||||
delete image.onerror;
|
|
||||||
(image.naturalWidth >= minWidth ? resolve : reject)(image);
|
|
||||||
};
|
|
||||||
Object.assign(image, {onload: handler, onerror: handler, src});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Load an external script
|
|
||||||
loadScript(url) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
loadjs(url, {
|
|
||||||
success: resolve,
|
|
||||||
error: reject,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Load an external SVG sprite
|
|
||||||
loadSprite(url, id) {
|
|
||||||
if (!utils.is.string(url)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefix = 'cache';
|
|
||||||
const hasId = utils.is.string(id);
|
|
||||||
let isCached = false;
|
|
||||||
|
|
||||||
const exists = () => document.getElementById(id) !== null;
|
|
||||||
|
|
||||||
const update = (container, data) => {
|
|
||||||
container.innerHTML = data;
|
|
||||||
|
|
||||||
// Check again incase of race condition
|
|
||||||
if (hasId && exists()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject the SVG to the body
|
|
||||||
document.body.insertAdjacentElement('afterbegin', container);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only load once if ID set
|
|
||||||
if (!hasId || !exists()) {
|
|
||||||
const useStorage = Storage.supported;
|
|
||||||
|
|
||||||
// Create container
|
|
||||||
const container = document.createElement('div');
|
|
||||||
utils.toggleHidden(container, true);
|
|
||||||
|
|
||||||
if (hasId) {
|
|
||||||
container.setAttribute('id', id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check in cache
|
|
||||||
if (useStorage) {
|
|
||||||
const cached = window.localStorage.getItem(`${prefix}-${id}`);
|
|
||||||
isCached = cached !== null;
|
|
||||||
|
|
||||||
if (isCached) {
|
|
||||||
const data = JSON.parse(cached);
|
|
||||||
update(container, data.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the sprite
|
|
||||||
utils
|
|
||||||
.fetch(url)
|
|
||||||
.then(result => {
|
|
||||||
if (utils.is.empty(result)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useStorage) {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
`${prefix}-${id}`,
|
|
||||||
JSON.stringify({
|
|
||||||
content: result,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(container, result);
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Generate a random ID
|
|
||||||
generateId(prefix) {
|
|
||||||
return `${prefix}-${Math.floor(Math.random() * 10000)}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Wrap an element
|
|
||||||
wrap(elements, wrapper) {
|
|
||||||
// Convert `elements` to an array, if necessary.
|
|
||||||
const targets = elements.length ? elements : [elements];
|
|
||||||
|
|
||||||
// Loops backwards to prevent having to clone the wrapper on the
|
|
||||||
// first element (see `child` below).
|
|
||||||
Array.from(targets)
|
|
||||||
.reverse()
|
|
||||||
.forEach((element, index) => {
|
|
||||||
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
|
|
||||||
|
|
||||||
// Cache the current parent and sibling.
|
|
||||||
const parent = element.parentNode;
|
|
||||||
const sibling = element.nextSibling;
|
|
||||||
|
|
||||||
// Wrap the element (is automatically removed from its current
|
|
||||||
// parent).
|
|
||||||
child.appendChild(element);
|
|
||||||
|
|
||||||
// If the element had a sibling, insert the wrapper before
|
|
||||||
// the sibling to maintain the HTML structure; otherwise, just
|
|
||||||
// append it to the parent.
|
|
||||||
if (sibling) {
|
|
||||||
parent.insertBefore(child, sibling);
|
|
||||||
} else {
|
|
||||||
parent.appendChild(child);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Create a DocumentFragment
|
|
||||||
createElement(type, attributes, text) {
|
|
||||||
// Create a new <element>
|
|
||||||
const element = document.createElement(type);
|
|
||||||
|
|
||||||
// Set all passed attributes
|
|
||||||
if (utils.is.object(attributes)) {
|
|
||||||
utils.setAttributes(element, attributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add text node
|
|
||||||
if (utils.is.string(text)) {
|
|
||||||
element.innerText = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return built element
|
|
||||||
return element;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Inaert an element after another
|
|
||||||
insertAfter(element, target) {
|
|
||||||
target.parentNode.insertBefore(element, target.nextSibling);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Insert a DocumentFragment
|
|
||||||
insertElement(type, parent, attributes, text) {
|
|
||||||
// Inject the new <element>
|
|
||||||
parent.appendChild(utils.createElement(type, attributes, text));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Remove element(s)
|
|
||||||
removeElement(element) {
|
|
||||||
if (utils.is.nodeList(element) || utils.is.array(element)) {
|
|
||||||
Array.from(element).forEach(utils.removeElement);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
element.parentNode.removeChild(element);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Remove all child elements
|
|
||||||
emptyElement(element) {
|
|
||||||
let { length } = element.childNodes;
|
|
||||||
|
|
||||||
while (length > 0) {
|
|
||||||
element.removeChild(element.lastChild);
|
|
||||||
length -= 1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Replace element
|
|
||||||
replaceElement(newChild, oldChild) {
|
|
||||||
if (!utils.is.element(oldChild) || !utils.is.element(oldChild.parentNode) || !utils.is.element(newChild)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
oldChild.parentNode.replaceChild(newChild, oldChild);
|
|
||||||
|
|
||||||
return newChild;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Set attributes
|
|
||||||
setAttributes(element, attributes) {
|
|
||||||
if (!utils.is.element(element) || utils.is.empty(attributes)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(attributes).forEach(([
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
]) => {
|
|
||||||
element.setAttribute(key, value);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get an attribute object from a string selector
|
|
||||||
getAttributesFromSelector(sel, existingAttributes) {
|
|
||||||
// For example:
|
|
||||||
// '.test' to { class: 'test' }
|
|
||||||
// '#test' to { id: 'test' }
|
|
||||||
// '[data-test="test"]' to { 'data-test': 'test' }
|
|
||||||
|
|
||||||
if (!utils.is.string(sel) || utils.is.empty(sel)) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const attributes = {};
|
|
||||||
const existing = existingAttributes;
|
|
||||||
|
|
||||||
sel.split(',').forEach(s => {
|
|
||||||
// Remove whitespace
|
|
||||||
const selector = s.trim();
|
|
||||||
const className = selector.replace('.', '');
|
|
||||||
const stripped = selector.replace(/[[\]]/g, '');
|
|
||||||
|
|
||||||
// Get the parts and value
|
|
||||||
const parts = stripped.split('=');
|
|
||||||
const key = parts[0];
|
|
||||||
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
|
|
||||||
|
|
||||||
// Get the first character
|
|
||||||
const start = selector.charAt(0);
|
|
||||||
|
|
||||||
switch (start) {
|
|
||||||
case '.':
|
|
||||||
// Add to existing classname
|
|
||||||
if (utils.is.object(existing) && utils.is.string(existing.class)) {
|
|
||||||
existing.class += ` ${className}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
attributes.class = className;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '#':
|
|
||||||
// ID selector
|
|
||||||
attributes.id = selector.replace('#', '');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '[':
|
|
||||||
// Attribute selector
|
|
||||||
attributes[key] = value;
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return attributes;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Toggle hidden
|
|
||||||
toggleHidden(element, hidden) {
|
|
||||||
if (!utils.is.element(element)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let hide = hidden;
|
|
||||||
|
|
||||||
if (!utils.is.boolean(hide)) {
|
|
||||||
hide = !element.hasAttribute('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hide) {
|
|
||||||
element.setAttribute('hidden', '');
|
|
||||||
} else {
|
|
||||||
element.removeAttribute('hidden');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
|
|
||||||
toggleClass(element, className, force) {
|
|
||||||
if (utils.is.element(element)) {
|
|
||||||
let method = 'toggle';
|
|
||||||
if (typeof force !== 'undefined') {
|
|
||||||
method = force ? 'add' : 'remove';
|
|
||||||
}
|
|
||||||
|
|
||||||
element.classList[method](className);
|
|
||||||
return element.classList.contains(className);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Has class name
|
|
||||||
hasClass(element, className) {
|
|
||||||
return utils.is.element(element) && element.classList.contains(className);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Element matches selector
|
|
||||||
matches(element, selector) {
|
|
||||||
const prototype = { Element };
|
|
||||||
|
|
||||||
function match() {
|
|
||||||
return Array.from(document.querySelectorAll(selector)).includes(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
const matches = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;
|
|
||||||
|
|
||||||
return matches.call(element, selector);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Find all elements
|
|
||||||
getElements(selector) {
|
|
||||||
return this.elements.container.querySelectorAll(selector);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Find a single element
|
|
||||||
getElement(selector) {
|
|
||||||
return this.elements.container.querySelector(selector);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get the focused element
|
|
||||||
getFocusElement() {
|
|
||||||
let focused = document.activeElement;
|
|
||||||
|
|
||||||
if (!focused || focused === document.body) {
|
|
||||||
focused = null;
|
|
||||||
} else {
|
|
||||||
focused = document.querySelector(':focus');
|
|
||||||
}
|
|
||||||
|
|
||||||
return focused;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Trap focus inside container
|
|
||||||
trapFocus(element = null, toggle = false) {
|
|
||||||
if (!utils.is.element(element)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
|
|
||||||
const first = focusable[0];
|
|
||||||
const last = focusable[focusable.length - 1];
|
|
||||||
|
|
||||||
const trap = event => {
|
|
||||||
// Bail if not tab key or not fullscreen
|
|
||||||
if (event.key !== 'Tab' || event.keyCode !== 9) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current focused element
|
|
||||||
const focused = utils.getFocusElement();
|
|
||||||
|
|
||||||
if (focused === last && !event.shiftKey) {
|
|
||||||
// Move focus to first element that can be tabbed if Shift isn't used
|
|
||||||
first.focus();
|
|
||||||
event.preventDefault();
|
|
||||||
} else if (focused === first && event.shiftKey) {
|
|
||||||
// Move focus to last element that can be tabbed if Shift is used
|
|
||||||
last.focus();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (toggle) {
|
|
||||||
utils.on(this.elements.container, 'keydown', trap, false);
|
|
||||||
} else {
|
|
||||||
utils.off(this.elements.container, 'keydown', trap, false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Toggle event listener
|
|
||||||
toggleListener(elements, event, callback, toggle = false, passive = true, capture = false) {
|
|
||||||
// Bail if no elemetns, event, or callback
|
|
||||||
if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a nodelist is passed, call itself on each node
|
|
||||||
if (utils.is.nodeList(elements) || utils.is.array(elements)) {
|
|
||||||
// Create listener for each node
|
|
||||||
Array.from(elements).forEach(element => {
|
|
||||||
if (element instanceof Node) {
|
|
||||||
utils.toggleListener.call(null, element, event, callback, toggle, passive, capture);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow multiple events
|
|
||||||
const events = event.split(' ');
|
|
||||||
|
|
||||||
// Build options
|
|
||||||
// Default to just the capture boolean for browsers with no passive listener support
|
|
||||||
let options = capture;
|
|
||||||
|
|
||||||
// If passive events listeners are supported
|
|
||||||
if (support.passiveListeners) {
|
|
||||||
options = {
|
|
||||||
// Whether the listener can be passive (i.e. default never prevented)
|
|
||||||
passive,
|
|
||||||
// Whether the listener is a capturing listener or not
|
|
||||||
capture,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a single node is passed, bind the event listener
|
|
||||||
events.forEach(type => {
|
|
||||||
elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Bind event handler
|
|
||||||
on(element, events = '', callback, passive = true, capture = false) {
|
|
||||||
utils.toggleListener(element, events, callback, true, passive, capture);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Unbind event handler
|
|
||||||
off(element, events = '', callback, passive = true, capture = false) {
|
|
||||||
utils.toggleListener(element, events, callback, false, passive, capture);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Trigger event
|
|
||||||
dispatchEvent(element, type = '', bubbles = false, detail = {}) {
|
|
||||||
// Bail if no element
|
|
||||||
if (!utils.is.element(element) || utils.is.empty(type)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and dispatch the event
|
|
||||||
const event = new CustomEvent(type, {
|
|
||||||
bubbles,
|
|
||||||
detail: Object.assign({}, detail, {
|
|
||||||
plyr: this,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dispatch the event
|
|
||||||
element.dispatchEvent(event);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Toggle aria-pressed state on a toggle button
|
|
||||||
// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
|
|
||||||
toggleState(element, input) {
|
|
||||||
// If multiple elements passed
|
|
||||||
if (utils.is.array(element) || utils.is.nodeList(element)) {
|
|
||||||
Array.from(element).forEach(target => utils.toggleState(target, input));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bail if no target
|
|
||||||
if (!utils.is.element(element)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get state
|
|
||||||
const pressed = element.getAttribute('aria-pressed') === 'true';
|
|
||||||
const state = utils.is.boolean(input) ? input : !pressed;
|
|
||||||
|
|
||||||
// Set the attribute on target
|
|
||||||
element.setAttribute('aria-pressed', state);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Format string
|
|
||||||
format(input, ...args) {
|
|
||||||
if (utils.is.empty(input)) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
return input.toString().replace(/{(\d+)}/g, (match, i) => (utils.is.string(args[i]) ? args[i] : ''));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get percentage
|
|
||||||
getPercentage(current, max) {
|
|
||||||
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (current / max * 100).toFixed(2);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Time helpers
|
|
||||||
getHours(value) {
|
|
||||||
return parseInt((value / 60 / 60) % 60, 10);
|
|
||||||
},
|
|
||||||
getMinutes(value) {
|
|
||||||
return parseInt((value / 60) % 60, 10);
|
|
||||||
},
|
|
||||||
getSeconds(value) {
|
|
||||||
return parseInt(value % 60, 10);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Format time to UI friendly string
|
|
||||||
formatTime(time = 0, displayHours = false, inverted = false) {
|
|
||||||
// Bail if the value isn't a number
|
|
||||||
if (!utils.is.number(time)) {
|
|
||||||
return utils.formatTime(null, displayHours, inverted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format time component to add leading zero
|
|
||||||
const format = value => `0${value}`.slice(-2);
|
|
||||||
|
|
||||||
// Breakdown to hours, mins, secs
|
|
||||||
let hours = utils.getHours(time);
|
|
||||||
const mins = utils.getMinutes(time);
|
|
||||||
const secs = utils.getSeconds(time);
|
|
||||||
|
|
||||||
// Do we need to display hours?
|
|
||||||
if (displayHours || hours > 0) {
|
|
||||||
hours = `${hours}:`;
|
|
||||||
} else {
|
|
||||||
hours = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render
|
|
||||||
return `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Replace all occurances of a string in a string
|
|
||||||
replaceAll(input = '', find = '', replace = '') {
|
|
||||||
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
|
|
||||||
},
|
|
||||||
|
|
||||||
// Convert to title case
|
|
||||||
toTitleCase(input = '') {
|
|
||||||
return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
|
|
||||||
},
|
|
||||||
|
|
||||||
// Convert string to pascalCase
|
|
||||||
toPascalCase(input = '') {
|
|
||||||
let string = input.toString();
|
|
||||||
|
|
||||||
// Convert kebab case
|
|
||||||
string = utils.replaceAll(string, '-', ' ');
|
|
||||||
|
|
||||||
// Convert snake case
|
|
||||||
string = utils.replaceAll(string, '_', ' ');
|
|
||||||
|
|
||||||
// Convert to title case
|
|
||||||
string = utils.toTitleCase(string);
|
|
||||||
|
|
||||||
// Convert to pascal case
|
|
||||||
return utils.replaceAll(string, ' ', '');
|
|
||||||
},
|
|
||||||
|
|
||||||
// Convert string to pascalCase
|
|
||||||
toCamelCase(input = '') {
|
|
||||||
let string = input.toString();
|
|
||||||
|
|
||||||
// Convert to pascal case
|
|
||||||
string = utils.toPascalCase(string);
|
|
||||||
|
|
||||||
// Convert first character to lowercase
|
|
||||||
return string.charAt(0).toLowerCase() + string.slice(1);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Deep extend destination object with N more objects
|
|
||||||
extend(target = {}, ...sources) {
|
|
||||||
if (!sources.length) {
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = sources.shift();
|
|
||||||
|
|
||||||
if (!utils.is.object(source)) {
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(source).forEach(key => {
|
|
||||||
if (utils.is.object(source[key])) {
|
|
||||||
if (!Object.keys(target).includes(key)) {
|
|
||||||
Object.assign(target, { [key]: {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.extend(target[key], source[key]);
|
|
||||||
} else {
|
|
||||||
Object.assign(target, { [key]: source[key] });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return utils.extend(target, ...sources);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Remove duplicates in an array
|
|
||||||
dedupe(array) {
|
|
||||||
if (!utils.is.array(array)) {
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
return array.filter((item, index) => array.indexOf(item) === index);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Clone nested objects
|
|
||||||
cloneDeep(object) {
|
|
||||||
return JSON.parse(JSON.stringify(object));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get a nested value in an object
|
|
||||||
getDeep(object, path) {
|
|
||||||
return path.split('.').reduce((obj, key) => obj && obj[key], object);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get the closest value in an array
|
|
||||||
closest(array, value) {
|
|
||||||
if (!utils.is.array(array) || !array.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get the provider for a given URL
|
|
||||||
getProviderByUrl(url) {
|
|
||||||
// YouTube
|
|
||||||
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) {
|
|
||||||
return providers.youtube;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vimeo
|
|
||||||
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
|
|
||||||
return providers.vimeo;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Parse YouTube ID from URL
|
|
||||||
parseYouTubeId(url) {
|
|
||||||
if (utils.is.empty(url)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
|
|
||||||
return url.match(regex) ? RegExp.$2 : url;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Parse Vimeo ID from URL
|
|
||||||
parseVimeoId(url) {
|
|
||||||
if (utils.is.empty(url)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utils.is.number(Number(url))) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
|
|
||||||
return url.match(regex) ? RegExp.$2 : url;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Convert a URL to a location object
|
|
||||||
parseUrl(url) {
|
|
||||||
const parser = document.createElement('a');
|
|
||||||
parser.href = url;
|
|
||||||
return parser;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get URL query parameters
|
|
||||||
getUrlParams(input) {
|
|
||||||
let search = input;
|
|
||||||
|
|
||||||
// Parse URL if needed
|
|
||||||
if (input.startsWith('http://') || input.startsWith('https://')) {
|
|
||||||
({ search } = utils.parseUrl(input));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utils.is.empty(search)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashes = search.slice(search.indexOf('?') + 1).split('&');
|
|
||||||
|
|
||||||
return hashes.reduce((params, hash) => {
|
|
||||||
const [
|
|
||||||
key,
|
|
||||||
val,
|
|
||||||
] = hash.split('=');
|
|
||||||
|
|
||||||
return Object.assign(params, { [key]: decodeURIComponent(val) });
|
|
||||||
}, {});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Convert object to URL parameters
|
|
||||||
buildUrlParams(input) {
|
|
||||||
if (!utils.is.object(input)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.keys(input)
|
|
||||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(input[key])}`)
|
|
||||||
.join('&');
|
|
||||||
},
|
|
||||||
|
|
||||||
// Remove HTML from a string
|
|
||||||
stripHTML(source) {
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
fragment.appendChild(element);
|
|
||||||
element.innerHTML = source;
|
|
||||||
return fragment.firstChild.innerText;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Like outerHTML, but also works for DocumentFragment
|
|
||||||
getHTML(element) {
|
|
||||||
const wrapper = document.createElement('div');
|
|
||||||
wrapper.appendChild(element);
|
|
||||||
return wrapper.innerHTML;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get aspect ratio for dimensions
|
|
||||||
getAspectRatio(width, height) {
|
|
||||||
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
|
|
||||||
const ratio = getRatio(width, height);
|
|
||||||
return `${width / ratio}:${height / ratio}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get the transition end event
|
|
||||||
get transitionEndEvent() {
|
|
||||||
const element = document.createElement('span');
|
|
||||||
|
|
||||||
const events = {
|
|
||||||
WebkitTransition: 'webkitTransitionEnd',
|
|
||||||
MozTransition: 'transitionend',
|
|
||||||
OTransition: 'oTransitionEnd otransitionend',
|
|
||||||
transition: 'transitionend',
|
|
||||||
};
|
|
||||||
|
|
||||||
const type = Object.keys(events).find(event => element.style[event] !== undefined);
|
|
||||||
|
|
||||||
return utils.is.string(type) ? events[type] : false;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Force repaint of element
|
|
||||||
repaint(element) {
|
|
||||||
setTimeout(() => {
|
|
||||||
utils.toggleHidden(element, true);
|
|
||||||
element.offsetHeight; // eslint-disable-line
|
|
||||||
utils.toggleHidden(element, false);
|
|
||||||
}, 0);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default utils;
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Animation utils
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import { toggleHidden } from './elements';
|
||||||
|
import is from './is';
|
||||||
|
|
||||||
|
export const transitionEndEvent = (() => {
|
||||||
|
const element = document.createElement('span');
|
||||||
|
|
||||||
|
const events = {
|
||||||
|
WebkitTransition: 'webkitTransitionEnd',
|
||||||
|
MozTransition: 'transitionend',
|
||||||
|
OTransition: 'oTransitionEnd otransitionend',
|
||||||
|
transition: 'transitionend',
|
||||||
|
};
|
||||||
|
|
||||||
|
const type = Object.keys(events).find(event => element.style[event] !== undefined);
|
||||||
|
|
||||||
|
return is.string(type) ? events[type] : false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Force repaint of element
|
||||||
|
export function repaint(element) {
|
||||||
|
setTimeout(() => {
|
||||||
|
toggleHidden(element, true);
|
||||||
|
element.offsetHeight; // eslint-disable-line
|
||||||
|
toggleHidden(element, false);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Array utils
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import is from './is';
|
||||||
|
|
||||||
|
// Remove duplicates in an array
|
||||||
|
export function dedupe(array) {
|
||||||
|
if (!is.array(array)) {
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array.filter((item, index) => array.indexOf(item) === index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the closest value in an array
|
||||||
|
export function closest(array, value) {
|
||||||
|
if (!is.array(array) || !array.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Browser sniffing
|
||||||
|
// Unfortunately, due to mixed support, UA sniffing is required
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
const browser = {
|
||||||
|
isIE: /* @cc_on!@ */ false || !!document.documentMode,
|
||||||
|
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
|
||||||
|
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
|
||||||
|
isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default browser;
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Element utils
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import { toggleListener } from './events';
|
||||||
|
import is from './is';
|
||||||
|
|
||||||
|
// Wrap an element
|
||||||
|
export function wrap(elements, wrapper) {
|
||||||
|
// Convert `elements` to an array, if necessary.
|
||||||
|
const targets = elements.length ? elements : [elements];
|
||||||
|
|
||||||
|
// Loops backwards to prevent having to clone the wrapper on the
|
||||||
|
// first element (see `child` below).
|
||||||
|
Array.from(targets)
|
||||||
|
.reverse()
|
||||||
|
.forEach((element, index) => {
|
||||||
|
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
|
||||||
|
|
||||||
|
// Cache the current parent and sibling.
|
||||||
|
const parent = element.parentNode;
|
||||||
|
const sibling = element.nextSibling;
|
||||||
|
|
||||||
|
// Wrap the element (is automatically removed from its current
|
||||||
|
// parent).
|
||||||
|
child.appendChild(element);
|
||||||
|
|
||||||
|
// If the element had a sibling, insert the wrapper before
|
||||||
|
// the sibling to maintain the HTML structure; otherwise, just
|
||||||
|
// append it to the parent.
|
||||||
|
if (sibling) {
|
||||||
|
parent.insertBefore(child, sibling);
|
||||||
|
} else {
|
||||||
|
parent.appendChild(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set attributes
|
||||||
|
export function setAttributes(element, attributes) {
|
||||||
|
if (!is.element(element) || is.empty(attributes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume null and undefined attributes should be left out,
|
||||||
|
// Setting them would otherwise convert them to "null" and "undefined"
|
||||||
|
Object.entries(attributes)
|
||||||
|
.filter(([, value]) => !is.nullOrUndefined(value))
|
||||||
|
.forEach(([key, value]) => element.setAttribute(key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a DocumentFragment
|
||||||
|
export function createElement(type, attributes, text) {
|
||||||
|
// Create a new <element>
|
||||||
|
const element = document.createElement(type);
|
||||||
|
|
||||||
|
// Set all passed attributes
|
||||||
|
if (is.object(attributes)) {
|
||||||
|
setAttributes(element, attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add text node
|
||||||
|
if (is.string(text)) {
|
||||||
|
element.innerText = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return built element
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inaert an element after another
|
||||||
|
export function insertAfter(element, target) {
|
||||||
|
target.parentNode.insertBefore(element, target.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert a DocumentFragment
|
||||||
|
export function insertElement(type, parent, attributes, text) {
|
||||||
|
// Inject the new <element>
|
||||||
|
parent.appendChild(createElement(type, attributes, text));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove element(s)
|
||||||
|
export function removeElement(element) {
|
||||||
|
if (is.nodeList(element) || is.array(element)) {
|
||||||
|
Array.from(element).forEach(removeElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is.element(element) || !is.element(element.parentNode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.parentNode.removeChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all child elements
|
||||||
|
export function emptyElement(element) {
|
||||||
|
let { length } = element.childNodes;
|
||||||
|
|
||||||
|
while (length > 0) {
|
||||||
|
element.removeChild(element.lastChild);
|
||||||
|
length -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace element
|
||||||
|
export function replaceElement(newChild, oldChild) {
|
||||||
|
if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
oldChild.parentNode.replaceChild(newChild, oldChild);
|
||||||
|
|
||||||
|
return newChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get an attribute object from a string selector
|
||||||
|
export function getAttributesFromSelector(sel, existingAttributes) {
|
||||||
|
// For example:
|
||||||
|
// '.test' to { class: 'test' }
|
||||||
|
// '#test' to { id: 'test' }
|
||||||
|
// '[data-test="test"]' to { 'data-test': 'test' }
|
||||||
|
|
||||||
|
if (!is.string(sel) || is.empty(sel)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = {};
|
||||||
|
const existing = existingAttributes;
|
||||||
|
|
||||||
|
sel.split(',').forEach(s => {
|
||||||
|
// Remove whitespace
|
||||||
|
const selector = s.trim();
|
||||||
|
const className = selector.replace('.', '');
|
||||||
|
const stripped = selector.replace(/[[\]]/g, '');
|
||||||
|
|
||||||
|
// Get the parts and value
|
||||||
|
const parts = stripped.split('=');
|
||||||
|
const key = parts[0];
|
||||||
|
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
|
||||||
|
|
||||||
|
// Get the first character
|
||||||
|
const start = selector.charAt(0);
|
||||||
|
|
||||||
|
switch (start) {
|
||||||
|
case '.':
|
||||||
|
// Add to existing classname
|
||||||
|
if (is.object(existing) && is.string(existing.class)) {
|
||||||
|
existing.class += ` ${className}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes.class = className;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '#':
|
||||||
|
// ID selector
|
||||||
|
attributes.id = selector.replace('#', '');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '[':
|
||||||
|
// Attribute selector
|
||||||
|
attributes[key] = value;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle hidden
|
||||||
|
export function toggleHidden(element, hidden) {
|
||||||
|
if (!is.element(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hide = hidden;
|
||||||
|
|
||||||
|
if (!is.boolean(hide)) {
|
||||||
|
hide = !element.hasAttribute('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hide) {
|
||||||
|
element.setAttribute('hidden', '');
|
||||||
|
} else {
|
||||||
|
element.removeAttribute('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
|
||||||
|
export function toggleClass(element, className, force) {
|
||||||
|
if (is.element(element)) {
|
||||||
|
let method = 'toggle';
|
||||||
|
if (typeof force !== 'undefined') {
|
||||||
|
method = force ? 'add' : 'remove';
|
||||||
|
}
|
||||||
|
|
||||||
|
element.classList[method](className);
|
||||||
|
return element.classList.contains(className);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has class name
|
||||||
|
export function hasClass(element, className) {
|
||||||
|
return is.element(element) && element.classList.contains(className);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Element matches selector
|
||||||
|
export function matches(element, selector) {
|
||||||
|
const prototype = { Element };
|
||||||
|
|
||||||
|
function match() {
|
||||||
|
return Array.from(document.querySelectorAll(selector)).includes(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches =
|
||||||
|
prototype.matches ||
|
||||||
|
prototype.webkitMatchesSelector ||
|
||||||
|
prototype.mozMatchesSelector ||
|
||||||
|
prototype.msMatchesSelector ||
|
||||||
|
match;
|
||||||
|
|
||||||
|
return matches.call(element, selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all elements
|
||||||
|
export function getElements(selector) {
|
||||||
|
return this.elements.container.querySelectorAll(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a single element
|
||||||
|
export function getElement(selector) {
|
||||||
|
return this.elements.container.querySelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the focused element
|
||||||
|
export function getFocusElement() {
|
||||||
|
let focused = document.activeElement;
|
||||||
|
|
||||||
|
if (!focused || focused === document.body) {
|
||||||
|
focused = null;
|
||||||
|
} else {
|
||||||
|
focused = document.querySelector(':focus');
|
||||||
|
}
|
||||||
|
|
||||||
|
return focused;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trap focus inside container
|
||||||
|
export function trapFocus(element = null, toggle = false) {
|
||||||
|
if (!is.element(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
|
||||||
|
const first = focusable[0];
|
||||||
|
const last = focusable[focusable.length - 1];
|
||||||
|
|
||||||
|
const trap = event => {
|
||||||
|
// Bail if not tab key or not fullscreen
|
||||||
|
if (event.key !== 'Tab' || event.keyCode !== 9) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current focused element
|
||||||
|
const focused = getFocusElement();
|
||||||
|
|
||||||
|
if (focused === last && !event.shiftKey) {
|
||||||
|
// Move focus to first element that can be tabbed if Shift isn't used
|
||||||
|
first.focus();
|
||||||
|
event.preventDefault();
|
||||||
|
} else if (focused === first && event.shiftKey) {
|
||||||
|
// Move focus to last element that can be tabbed if Shift is used
|
||||||
|
last.focus();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Event utils
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import is from './is';
|
||||||
|
|
||||||
|
// Check for passive event listener support
|
||||||
|
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
|
||||||
|
// https://www.youtube.com/watch?v=NPM6172J22g
|
||||||
|
const supportsPassiveListeners = (() => {
|
||||||
|
// Test via a getter in the options object to see if the passive property is accessed
|
||||||
|
let supported = false;
|
||||||
|
try {
|
||||||
|
const options = Object.defineProperty({}, 'passive', {
|
||||||
|
get() {
|
||||||
|
supported = true;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.addEventListener('test', null, options);
|
||||||
|
window.removeEventListener('test', null, options);
|
||||||
|
} catch (e) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
return supported;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Toggle event listener
|
||||||
|
export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {
|
||||||
|
// Bail if no element, event, or callback
|
||||||
|
if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow multiple events
|
||||||
|
const events = event.split(' ');
|
||||||
|
|
||||||
|
// Build options
|
||||||
|
// Default to just the capture boolean for browsers with no passive listener support
|
||||||
|
let options = capture;
|
||||||
|
|
||||||
|
// If passive events listeners are supported
|
||||||
|
if (supportsPassiveListeners) {
|
||||||
|
options = {
|
||||||
|
// Whether the listener can be passive (i.e. default never prevented)
|
||||||
|
passive,
|
||||||
|
// Whether the listener is a capturing listener or not
|
||||||
|
capture,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a single node is passed, bind the event listener
|
||||||
|
events.forEach(type => {
|
||||||
|
if (this && this.eventListeners && toggle) {
|
||||||
|
// Cache event listener
|
||||||
|
this.eventListeners.push({ element, type, callback, options });
|
||||||
|
}
|
||||||
|
|
||||||
|
element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind event handler
|
||||||
|
export function on(element, events = '', callback, passive = true, capture = false) {
|
||||||
|
toggleListener.call(this, element, events, callback, true, passive, capture);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unbind event handler
|
||||||
|
export function off(element, events = '', callback, passive = true, capture = false) {
|
||||||
|
toggleListener.call(this, element, events, callback, false, passive, capture);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind once-only event handler
|
||||||
|
export function once(element, events = '', callback, passive = true, capture = false) {
|
||||||
|
function onceCallback(...args) {
|
||||||
|
off(element, events, onceCallback, passive, capture);
|
||||||
|
callback.apply(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleListener.call(this, element, events, onceCallback, true, passive, capture);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger event
|
||||||
|
export function triggerEvent(element, type = '', bubbles = false, detail = {}) {
|
||||||
|
// Bail if no element
|
||||||
|
if (!is.element(element) || is.empty(type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and dispatch the event
|
||||||
|
const event = new CustomEvent(type, {
|
||||||
|
bubbles,
|
||||||
|
detail: Object.assign({}, detail, {
|
||||||
|
plyr: this,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dispatch the event
|
||||||
|
element.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unbind all cached event listeners
|
||||||
|
export function unbindListeners() {
|
||||||
|
if (this && this.eventListeners) {
|
||||||
|
this.eventListeners.forEach(item => {
|
||||||
|
const { element, type, callback, options } = item;
|
||||||
|
element.removeEventListener(type, callback, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventListeners = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run method when / if player is ready
|
||||||
|
export function ready() {
|
||||||
|
return new Promise(
|
||||||
|
resolve => (this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)),
|
||||||
|
).then(() => {});
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Fetch wrapper
|
||||||
|
// Using XHR to avoid issues with older browsers
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
export default function fetch(url, responseType = 'text') {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const request = new XMLHttpRequest();
|
||||||
|
|
||||||
|
// Check for CORS support
|
||||||
|
if (!('withCredentials' in request)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
request.addEventListener('load', () => {
|
||||||
|
if (responseType === 'text') {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(request.responseText));
|
||||||
|
} catch (e) {
|
||||||
|
resolve(request.responseText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(request.response);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
request.addEventListener('error', () => {
|
||||||
|
throw new Error(request.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.open('GET', url, true);
|
||||||
|
|
||||||
|
// Set the required response type
|
||||||
|
request.responseType = responseType;
|
||||||
|
|
||||||
|
request.send();
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Type checking utils
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
const getConstructor = input => (input !== null && typeof input !== 'undefined' ? input.constructor : null);
|
||||||
|
const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);
|
||||||
|
const isNullOrUndefined = input => input === null || typeof input === 'undefined';
|
||||||
|
const isObject = input => getConstructor(input) === Object;
|
||||||
|
const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);
|
||||||
|
const isString = input => getConstructor(input) === String;
|
||||||
|
const isBoolean = input => getConstructor(input) === Boolean;
|
||||||
|
const isFunction = input => getConstructor(input) === Function;
|
||||||
|
const isArray = input => Array.isArray(input);
|
||||||
|
const isWeakMap = input => instanceOf(input, WeakMap);
|
||||||
|
const isNodeList = input => instanceOf(input, NodeList);
|
||||||
|
const isElement = input => instanceOf(input, Element);
|
||||||
|
const isTextNode = input => getConstructor(input) === Text;
|
||||||
|
const isEvent = input => instanceOf(input, Event);
|
||||||
|
const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
|
||||||
|
const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));
|
||||||
|
|
||||||
|
const isEmpty = input =>
|
||||||
|
isNullOrUndefined(input) ||
|
||||||
|
((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||
|
||||||
|
(isObject(input) && !Object.keys(input).length);
|
||||||
|
|
||||||
|
const isUrl = input => {
|
||||||
|
// Accept a URL object
|
||||||
|
if (instanceOf(input, window.URL)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the protocol if required
|
||||||
|
let string = input;
|
||||||
|
if (!input.startsWith('http://') || !input.startsWith('https://')) {
|
||||||
|
string = `http://${input}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return !isEmpty(new URL(string).hostname);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
nullOrUndefined: isNullOrUndefined,
|
||||||
|
object: isObject,
|
||||||
|
number: isNumber,
|
||||||
|
string: isString,
|
||||||
|
boolean: isBoolean,
|
||||||
|
function: isFunction,
|
||||||
|
array: isArray,
|
||||||
|
weakMap: isWeakMap,
|
||||||
|
nodeList: isNodeList,
|
||||||
|
element: isElement,
|
||||||
|
textNode: isTextNode,
|
||||||
|
event: isEvent,
|
||||||
|
cue: isCue,
|
||||||
|
track: isTrack,
|
||||||
|
url: isUrl,
|
||||||
|
empty: isEmpty,
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Load image avoiding xhr/fetch CORS issues
|
||||||
|
// Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded
|
||||||
|
// By default it checks if it is at least 1px, but you can add a second argument to change this
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
export default function loadImage(src, minWidth = 1) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const image = new Image();
|
||||||
|
|
||||||
|
const handler = () => {
|
||||||
|
delete image.onload;
|
||||||
|
delete image.onerror;
|
||||||
|
(image.naturalWidth >= minWidth ? resolve : reject)(image);
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(image, { onload: handler, onerror: handler, src });
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Load an external script
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import loadjs from 'loadjs';
|
||||||
|
|
||||||
|
export default function loadScript(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
loadjs(url, {
|
||||||
|
success: resolve,
|
||||||
|
error: reject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Sprite loader
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import Storage from '../storage';
|
||||||
|
import fetch from './fetch';
|
||||||
|
import is from './is';
|
||||||
|
|
||||||
|
// Load an external SVG sprite
|
||||||
|
export default function loadSprite(url, id) {
|
||||||
|
if (!is.string(url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = 'cache';
|
||||||
|
const hasId = is.string(id);
|
||||||
|
let isCached = false;
|
||||||
|
|
||||||
|
const exists = () => document.getElementById(id) !== null;
|
||||||
|
|
||||||
|
const update = (container, data) => {
|
||||||
|
container.innerHTML = data;
|
||||||
|
|
||||||
|
// Check again incase of race condition
|
||||||
|
if (hasId && exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject the SVG to the body
|
||||||
|
document.body.insertAdjacentElement('afterbegin', container);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only load once if ID set
|
||||||
|
if (!hasId || !exists()) {
|
||||||
|
const useStorage = Storage.supported;
|
||||||
|
|
||||||
|
// Create container
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.setAttribute('hidden', '');
|
||||||
|
|
||||||
|
if (hasId) {
|
||||||
|
container.setAttribute('id', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check in cache
|
||||||
|
if (useStorage) {
|
||||||
|
const cached = window.localStorage.getItem(`${prefix}-${id}`);
|
||||||
|
isCached = cached !== null;
|
||||||
|
|
||||||
|
if (isCached) {
|
||||||
|
const data = JSON.parse(cached);
|
||||||
|
update(container, data.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the sprite
|
||||||
|
fetch(url)
|
||||||
|
.then(result => {
|
||||||
|
if (is.empty(result)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useStorage) {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${prefix}-${id}`,
|
||||||
|
JSON.stringify({
|
||||||
|
content: result,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(container, result);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Object utils
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import is from './is';
|
||||||
|
|
||||||
|
// Clone nested objects
|
||||||
|
export function cloneDeep(object) {
|
||||||
|
return JSON.parse(JSON.stringify(object));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a nested value in an object
|
||||||
|
export function getDeep(object, path) {
|
||||||
|
return path.split('.').reduce((obj, key) => obj && obj[key], object);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep extend destination object with N more objects
|
||||||
|
export function extend(target = {}, ...sources) {
|
||||||
|
if (!sources.length) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = sources.shift();
|
||||||
|
|
||||||
|
if (!is.object(source)) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(source).forEach(key => {
|
||||||
|
if (is.object(source[key])) {
|
||||||
|
if (!Object.keys(target).includes(key)) {
|
||||||
|
Object.assign(target, { [key]: {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(target[key], source[key]);
|
||||||
|
} else {
|
||||||
|
Object.assign(target, { [key]: source[key] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return extend(target, ...sources);
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// String utils
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import is from './is';
|
||||||
|
|
||||||
|
// Generate a random ID
|
||||||
|
export function generateId(prefix) {
|
||||||
|
return `${prefix}-${Math.floor(Math.random() * 10000)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format string
|
||||||
|
export function format(input, ...args) {
|
||||||
|
if (is.empty(input)) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
return input.toString().replace(/{(\d+)}/g, (match, i) => args[i].toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get percentage
|
||||||
|
export function getPercentage(current, max) {
|
||||||
|
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (current / max * 100).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace all occurances of a string in a string
|
||||||
|
export function replaceAll(input = '', find = '', replace = '') {
|
||||||
|
return input.replace(
|
||||||
|
new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'),
|
||||||
|
replace.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to title case
|
||||||
|
export function toTitleCase(input = '') {
|
||||||
|
return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert string to pascalCase
|
||||||
|
export function toPascalCase(input = '') {
|
||||||
|
let string = input.toString();
|
||||||
|
|
||||||
|
// Convert kebab case
|
||||||
|
string = replaceAll(string, '-', ' ');
|
||||||
|
|
||||||
|
// Convert snake case
|
||||||
|
string = replaceAll(string, '_', ' ');
|
||||||
|
|
||||||
|
// Convert to title case
|
||||||
|
string = toTitleCase(string);
|
||||||
|
|
||||||
|
// Convert to pascal case
|
||||||
|
return replaceAll(string, ' ', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert string to pascalCase
|
||||||
|
export function toCamelCase(input = '') {
|
||||||
|
let string = input.toString();
|
||||||
|
|
||||||
|
// Convert to pascal case
|
||||||
|
string = toPascalCase(string);
|
||||||
|
|
||||||
|
// Convert first character to lowercase
|
||||||
|
return string.charAt(0).toLowerCase() + string.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove HTML from a string
|
||||||
|
export function stripHTML(source) {
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
const element = document.createElement('div');
|
||||||
|
fragment.appendChild(element);
|
||||||
|
element.innerHTML = source;
|
||||||
|
return fragment.firstChild.innerText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Like outerHTML, but also works for DocumentFragment
|
||||||
|
export function getHTML(element) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.appendChild(element);
|
||||||
|
return wrapper.innerHTML;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Time utils
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import is from './is';
|
||||||
|
|
||||||
|
// Time helpers
|
||||||
|
export const getHours = value => parseInt((value / 60 / 60) % 60, 10);
|
||||||
|
export const getMinutes = value => parseInt((value / 60) % 60, 10);
|
||||||
|
export const getSeconds = value => parseInt(value % 60, 10);
|
||||||
|
|
||||||
|
// Format time to UI friendly string
|
||||||
|
export function formatTime(time = 0, displayHours = false, inverted = false) {
|
||||||
|
// Bail if the value isn't a number
|
||||||
|
if (!is.number(time)) {
|
||||||
|
return formatTime(null, displayHours, inverted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format time component to add leading zero
|
||||||
|
const format = value => `0${value}`.slice(-2);
|
||||||
|
|
||||||
|
// Breakdown to hours, mins, secs
|
||||||
|
let hours = getHours(time);
|
||||||
|
const mins = getMinutes(time);
|
||||||
|
const secs = getSeconds(time);
|
||||||
|
|
||||||
|
// Do we need to display hours?
|
||||||
|
if (displayHours || hours > 0) {
|
||||||
|
hours = `${hours}:`;
|
||||||
|
} else {
|
||||||
|
hours = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render
|
||||||
|
return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// URL utils
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
import is from './is';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a string to a URL object
|
||||||
|
* @param {string} input - the URL to be parsed
|
||||||
|
* @param {boolean} safe - failsafe parsing
|
||||||
|
*/
|
||||||
|
export function parseUrl(input, safe = true) {
|
||||||
|
let url = input;
|
||||||
|
|
||||||
|
if (safe) {
|
||||||
|
const parser = document.createElement('a');
|
||||||
|
parser.href = url;
|
||||||
|
url = parser.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new URL(url);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert object to URLSearchParams
|
||||||
|
export function buildUrlParams(input) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (is.object(input)) {
|
||||||
|
Object.entries(input).forEach(([key, value]) => {
|
||||||
|
params.set(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
@@ -34,10 +34,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Change icons on state change
|
// Change icons on state change
|
||||||
.plyr__control[aria-pressed='false'] .icon--pressed,
|
.plyr__control:not(.plyr__control--pressed) .icon--pressed,
|
||||||
.plyr__control[aria-pressed='true'] .icon--not-pressed,
|
.plyr__control.plyr__control--pressed .icon--not-pressed,
|
||||||
.plyr__control[aria-pressed='false'] .label--pressed,
|
.plyr__control:not(.plyr__control--pressed) .label--pressed,
|
||||||
.plyr__control[aria-pressed='true'] .label--not-pressed {
|
.plyr__control.plyr__control--pressed .label--not-pressed {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
.plyr__controls {
|
.plyr__controls {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
// Spacing
|
// Spacing
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
&:first-child,
|
&:first-child,
|
||||||
&:first-child + [data-plyr='pause'] {
|
&:first-child + [data-plyr='pause'] {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,13 +50,17 @@
|
|||||||
|
|
||||||
// Video controls
|
// Video controls
|
||||||
.plyr--video .plyr__controls {
|
.plyr--video .plyr__controls {
|
||||||
background: linear-gradient(rgba($plyr-video-controls-bg, 0), rgba($plyr-video-controls-bg, 0.7));
|
background: linear-gradient(
|
||||||
|
rgba($plyr-video-controls-bg, 0),
|
||||||
|
rgba($plyr-video-controls-bg, 0.7)
|
||||||
|
);
|
||||||
border-bottom-left-radius: inherit;
|
border-bottom-left-radius: inherit;
|
||||||
border-bottom-right-radius: inherit;
|
border-bottom-right-radius: inherit;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
color: $plyr-video-control-color;
|
color: $plyr-video-control-color;
|
||||||
left: 0;
|
left: 0;
|
||||||
padding: ($plyr-control-spacing * 3.5) $plyr-control-spacing $plyr-control-spacing;
|
padding: ($plyr-control-spacing * 3.5) $plyr-control-spacing
|
||||||
|
$plyr-control-spacing;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
|
transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
|
||||||
|
|||||||
@@ -27,11 +27,6 @@ $embed-padding: ((100 / 16) * 9);
|
|||||||
$height: 240;
|
$height: 240;
|
||||||
$offset: to-percentage(($height - $embed-padding) / ($height / 50));
|
$offset: to-percentage(($height - $embed-padding) / ($height / 50));
|
||||||
|
|
||||||
// To allow mouse events to be captured if full support
|
|
||||||
iframe {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only used for Vimeo
|
// Only used for Vimeo
|
||||||
> .plyr__video-embed__container {
|
> .plyr__video-embed__container {
|
||||||
padding-bottom: to-percentage($height);
|
padding-bottom: to-percentage($height);
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.plyr--stopped.plyr__poster-enabled .plyr__poster {
|
.plyr--stopped.plyr__poster-enabled .plyr__poster {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user