Compare commits
311 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d434c9af16 | ||
|
|
9ce1dd6808 | ||
|
|
28347a0d7e | ||
|
|
5f9d090c13 | ||
|
|
1d4869beee | ||
|
|
565b68a5e2 | ||
|
|
285282f366 | ||
|
|
02456d6ba3 | ||
|
|
1d920a2e58 | ||
|
|
ba67920025 | ||
|
|
b7953ff0fc | ||
|
|
6b8e0f25d6 | ||
|
|
895299a4b3 | ||
|
|
2d7638a230 | ||
|
|
ab028ea475 | ||
|
|
9157ac09ed | ||
|
|
da7524438b | ||
|
|
c64c8ac6c0 | ||
|
|
d700bb9f02 | ||
|
|
2b6208565f | ||
|
|
6bc447b916 | ||
|
|
4632614ced | ||
|
|
01c5428fc4 | ||
|
|
591c045c20 | ||
|
|
1d7ea197f3 | ||
|
|
1d0d6a3a92 | ||
|
|
6a92e60a67 | ||
|
|
40395a1ff5 | ||
|
|
9ea3a6923d | ||
|
|
675a853e2e | ||
|
|
950786b117 | ||
|
|
626e1dba64 | ||
|
|
cbd1596af4 | ||
|
|
6ea510c36e | ||
|
|
0acf228dcd | ||
|
|
91b19baf44 | ||
|
|
990d636019 | ||
|
|
b9c792b377 | ||
|
|
1b835cb657 | ||
|
|
d9f9dc770b | ||
|
|
dca2ff8cfd | ||
|
|
cf8e9341f2 | ||
|
|
5d1d247491 | ||
|
|
fbf4d27cd4 | ||
|
|
e0b70d53cd | ||
|
|
9f7725cec1 | ||
|
|
720cddff21 | ||
|
|
1c33098c42 | ||
|
|
b256c102e8 | ||
|
|
a3716fc491 | ||
|
|
14e276a11d | ||
|
|
fc85a63af4 | ||
|
|
f016a367b5 | ||
|
|
d41a90f0dd | ||
|
|
b4f24f90da | ||
|
|
9d751f4265 | ||
|
|
ff2c224254 | ||
|
|
1135b1acac | ||
|
|
30ffb30a97 | ||
|
|
e4a18a5c99 | ||
|
|
d1a10386c0 | ||
|
|
4d6ac4ade1 | ||
|
|
c2bfe489ea | ||
|
|
882952bba6 | ||
|
|
784f36921b | ||
|
|
b7dbdc133e | ||
|
|
02d06c464c | ||
|
|
cec2474295 | ||
|
|
6caa2f3fed | ||
|
|
3a01837561 | ||
|
|
ba4bdb335a | ||
|
|
ba236c4753 | ||
|
|
a204f7c1dd | ||
|
|
bacf9122de | ||
|
|
2fdc1eac94 | ||
|
|
5fd4391cd9 | ||
|
|
6991358428 | ||
|
|
b050fde276 | ||
|
|
32444c9851 | ||
|
|
b93dcc43ad | ||
|
|
6ff6ff1673 | ||
|
|
e1ff86820c | ||
|
|
7d22c361d1 | ||
|
|
951cccb6b0 | ||
|
|
438e425838 | ||
|
|
ddb5ad071e | ||
|
|
38060d986c | ||
|
|
8c74396459 | ||
|
|
a09d63e8f8 | ||
|
|
d74af9a73d | ||
|
|
60310693d3 | ||
|
|
5d08821c9b | ||
|
|
3316e40e7b | ||
|
|
dfe5985326 | ||
|
|
b27ad17c59 | ||
|
|
14b8b24e14 | ||
|
|
711fc5835f | ||
|
|
028be22dec | ||
|
|
3b5dc09285 | ||
|
|
8596e05df3 | ||
|
|
1aee8f53a7 | ||
|
|
a187d07807 | ||
|
|
3096dd9513 | ||
|
|
150b07f3ef | ||
|
|
ad7778e78d | ||
|
|
c8e776bbea | ||
|
|
c66dc8bf95 | ||
|
|
ea87b9c788 | ||
|
|
aa3378fd73 | ||
|
|
2cebf9b8a3 | ||
|
|
572aeacf5b | ||
|
|
b5592b0fb5 | ||
|
|
9adf35a444 | ||
|
|
39ff31e3e3 | ||
|
|
7e82ec6d70 | ||
|
|
f4d6a243cd | ||
|
|
31b5027b1e | ||
|
|
7750314058 | ||
|
|
16c134fc1e | ||
|
|
f00e81a976 | ||
|
|
32a4039f7c | ||
|
|
ba09bc32d3 | ||
|
|
d7195d5276 | ||
|
|
6a6fb8d41a | ||
|
|
c853adc760 | ||
|
|
ca041f2e0d | ||
|
|
87078d7a44 | ||
|
|
c300df5742 | ||
|
|
2ba56fda15 | ||
|
|
b444aa3a98 | ||
|
|
e8d883edba | ||
|
|
36b53d2cf0 | ||
|
|
2d4686a5f3 | ||
|
|
31861bd475 | ||
|
|
c3e163741f | ||
|
|
1c715bc889 | ||
|
|
35ef42246b | ||
|
|
cd3962ca32 | ||
|
|
da16c55427 | ||
|
|
bd8bacb6d5 | ||
|
|
0d393a5c92 | ||
|
|
136bb525a5 | ||
|
|
e85737d855 | ||
|
|
8882317080 | ||
|
|
5c02205a4f | ||
|
|
45f7e20233 | ||
|
|
b116e62f37 | ||
|
|
fa6282d0e2 | ||
|
|
80bdf04fde | ||
|
|
5d2c288721 | ||
|
|
3caa37fdf2 | ||
|
|
807efcbc46 | ||
|
|
03d1e3d3e8 | ||
|
|
664e2b4893 | ||
|
|
4cf36bf203 | ||
|
|
92f3728d80 | ||
|
|
29d3ae0b67 | ||
|
|
df91f7faac | ||
|
|
9a69ae2599 | ||
|
|
02321c35bc | ||
|
|
7c6316d824 | ||
|
|
776b0c4036 | ||
|
|
fa653a8859 | ||
|
|
b6b7db7327 | ||
|
|
d4b4303b8e | ||
|
|
102fb1a32f | ||
|
|
c77ba3ecf0 | ||
|
|
760f5f9469 | ||
|
|
0135e9c7f4 | ||
|
|
2426c256cc | ||
|
|
93eed08d14 | ||
|
|
5a164780d8 | ||
|
|
30989e4dbc | ||
|
|
967206c695 | ||
|
|
272d39c1d1 | ||
|
|
828abc8308 | ||
|
|
6455a6acc0 | ||
|
|
51cb03dc32 | ||
|
|
4d25a33cb0 | ||
|
|
e646207ed6 | ||
|
|
18b3f23c69 | ||
|
|
98dbb2e43f | ||
|
|
11e48b0181 | ||
|
|
3a07669e30 | ||
|
|
3e70f32555 | ||
|
|
b3d390076b | ||
|
|
80fbeeb415 | ||
|
|
8261ebd502 | ||
|
|
f75820fbd3 | ||
|
|
8b0d84a9a4 | ||
|
|
d582e43413 | ||
|
|
75082bc73f | ||
|
|
f7e9ee56d2 | ||
|
|
22af7f16ea | ||
|
|
9076d054b9 | ||
|
|
4eaa1a72b5 | ||
|
|
6f77e21e51 | ||
|
|
7910fea011 | ||
|
|
4c1ae8f3ce | ||
|
|
0733133510 | ||
|
|
8e0cde2949 | ||
|
|
423b7b276f | ||
|
|
cbd4a7cef4 | ||
|
|
391afe2814 | ||
|
|
68e3944bb8 | ||
|
|
2ee6ae16b2 | ||
|
|
8ce7425f73 | ||
|
|
1f63806c3f | ||
|
|
6cb822d56f | ||
|
|
9dee5acec6 | ||
|
|
ef85cb6195 | ||
|
|
dc8612fcd0 | ||
|
|
86c3cf648f | ||
|
|
37be1336fb | ||
|
|
3d7f80737b | ||
|
|
58a9321764 | ||
|
|
b600f387f0 | ||
|
|
2a97adbec6 | ||
|
|
853753a3da | ||
|
|
405bf9ce37 | ||
|
|
d5ea881729 | ||
|
|
391b9acd98 | ||
|
|
dda2d072aa | ||
|
|
3c1ba2397b | ||
|
|
adb3f35920 | ||
|
|
a58b8bf4bb | ||
|
|
99a26d65cf | ||
|
|
4915cf0120 | ||
|
|
66c5780616 | ||
|
|
e48b1d11ce | ||
|
|
ba91f23c50 | ||
|
|
ad4f303aa0 | ||
|
|
3b521f73bd | ||
|
|
27126b20bc | ||
|
|
68137aa789 | ||
|
|
2458eaa11b | ||
|
|
a97008aeeb | ||
|
|
a9b24f5e1f | ||
|
|
a9c4e77d1b | ||
|
|
dbe618c644 | ||
|
|
40f06e0b4c | ||
|
|
6aa21c1fae | ||
|
|
8015a961cb | ||
|
|
d2fa69bca6 | ||
|
|
d63182ecd5 | ||
|
|
145f2ae24f | ||
|
|
9c7e429b48 | ||
|
|
6f1366bd19 | ||
|
|
502d5977d7 | ||
|
|
353e19e746 | ||
|
|
12ab1ed144 | ||
|
|
11214caf77 | ||
|
|
c23b4576df | ||
|
|
a55bf00d0f | ||
|
|
49ed2cac4e | ||
|
|
44ef0bbc87 | ||
|
|
8f5b59c18c | ||
|
|
d06881783d | ||
|
|
da943b384c | ||
|
|
ad63af5096 | ||
|
|
09598f07bf | ||
|
|
ef7b30c1b8 | ||
|
|
5516db22c3 | ||
|
|
155add66bd | ||
|
|
39558cbabc | ||
|
|
4935c92b63 | ||
|
|
66f1f28646 | ||
|
|
48758bd5f0 | ||
|
|
2f26c80c88 | ||
|
|
be3ffc1f96 | ||
|
|
3509995756 | ||
|
|
88dc981f4b | ||
|
|
a65acbe43f | ||
|
|
6a1d6f13a2 | ||
|
|
71928443f3 | ||
|
|
10f366fe32 | ||
|
|
fcd82088a5 | ||
|
|
c9b6d9685c | ||
|
|
3069d77b74 | ||
|
|
b7367d8275 | ||
|
|
99ae4eb3c5 | ||
|
|
c7bf0c5c03 | ||
|
|
fd353225c2 | ||
|
|
ace682abbd | ||
|
|
b212b25a9e | ||
|
|
3c127afeb9 | ||
|
|
84eef1d747 | ||
|
|
8a0086397f | ||
|
|
75327f2242 | ||
|
|
81b41be750 | ||
|
|
6020f95e50 | ||
|
|
bc8a25d0da | ||
|
|
29e62a1e4f | ||
|
|
b12ec094c5 | ||
|
|
fea5e76b76 | ||
|
|
0cf5d25a7f | ||
|
|
977a8393f8 | ||
|
|
302fd93e86 | ||
|
|
70470ae8d2 | ||
|
|
425b39a762 | ||
|
|
550bd543e3 | ||
|
|
50a7c2fad6 | ||
|
|
95be6e721b | ||
|
|
dc2e012cc9 | ||
|
|
12c6282d14 | ||
|
|
996075decc | ||
|
|
848e798809 | ||
|
|
35f7ee9c59 | ||
|
|
bdd513635f | ||
|
|
b7b2e3c0aa | ||
|
|
3e0a911418 |
@@ -4,7 +4,7 @@ root = true
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
44
.eslintrc
44
.eslintrc
@@ -1,30 +1,18 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"extends": ["airbnb-base", "prettier"],
|
||||
"plugins": ["simple-import-sort", "import"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"globals": {
|
||||
"Plyr": false,
|
||||
"jQuery": false
|
||||
},
|
||||
"rules": {
|
||||
"import/no-cycle": "warn",
|
||||
"padding-line-between-statements": [
|
||||
"error",
|
||||
{
|
||||
"blankLine": "never",
|
||||
"prev": ["singleline-const", "singleline-let", "singleline-var"],
|
||||
"next": ["singleline-const", "singleline-let", "singleline-var"]
|
||||
}
|
||||
],
|
||||
"sort-imports": "off",
|
||||
"import/order": "off",
|
||||
"simple-import-sort/sort": "error"
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
}
|
||||
"parser": "babel-eslint",
|
||||
"extends": ["@sampotts/eslint-config/es6"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"globals": {
|
||||
"Plyr": false,
|
||||
"jQuery": false
|
||||
},
|
||||
"rules": {
|
||||
"import/no-cycle": "warn"
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
|
||||
2
.github/issue_template.md
vendored
2
.github/issue_template.md
vendored
@@ -1,3 +1,3 @@
|
||||
PLEASE USE OUR SPECIFIC ISSUE TEMPLATES for bug reports, features and improvement suggestions.
|
||||
|
||||
Our issue tracker is not for support questions. If you need help, follow our support instructions: https://github.com/sampotts/plyr/blob/master/contributing.md#support
|
||||
Our issue tracker is not for support questions. If you need help, follow our support instructions: https://github.com/sampotts/plyr/blob/master/CONTRIBUTING.md#support
|
||||
|
||||
5
.github/pull_request_template.md
vendored
5
.github/pull_request_template.md
vendored
@@ -1,8 +1,3 @@
|
||||
### Link to related issue (if applicable)
|
||||
|
||||
### Summary of proposed changes
|
||||
|
||||
### Checklist
|
||||
- [ ] Use `develop` as the base branch
|
||||
- [ ] Exclude the gulp build (`/dist` changes) from the PR
|
||||
- [ ] Test on [supported browsers](https://github.com/sampotts/plyr#browser-support)
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,8 +2,8 @@ node_modules
|
||||
.DS_Store
|
||||
credentials.json
|
||||
*.mp4
|
||||
!dist/blank.mp4
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
*.webm
|
||||
.idea/
|
||||
dist/
|
||||
|
||||
6
.gitpod.yml
Normal file
6
.gitpod.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
tasks:
|
||||
- before: npm install && npm i gulp -g
|
||||
command: gulp
|
||||
ports:
|
||||
- port: 3000
|
||||
onOpen: open-preview
|
||||
@@ -7,6 +7,6 @@ credentials.json
|
||||
deploy.json
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
*.mp4
|
||||
*.webm
|
||||
!dist/blank.mp4
|
||||
*.mp4
|
||||
!dist/blank.mp4
|
||||
|
||||
10
.prettierrc
10
.prettierrc
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
{
|
||||
"plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"],
|
||||
"extends": ["stylelint-config-recommended", "stylelint-config-sass-guidelines", "stylelint-config-prettier"],
|
||||
"rules": {
|
||||
"selector-class-pattern": null,
|
||||
"selector-no-qualifying-type": [
|
||||
true,
|
||||
{
|
||||
"ignore": ["attribute", "class"]
|
||||
}
|
||||
],
|
||||
"string-no-newline": null,
|
||||
"indentation": 4,
|
||||
"string-quotes": "single",
|
||||
"max-nesting-depth": 2,
|
||||
"plugin/selector-bem-pattern": {
|
||||
"preset": "bem",
|
||||
"componentName": "(([a-z0-9]+(?!-$)-?)+)",
|
||||
"componentSelectors": {
|
||||
"initial": "\\.{componentName}(((__|--)(([a-z0-9\\[\\]'=]+(?!-$)-?)+))+)?$"
|
||||
},
|
||||
"ignoreSelectors": [".*\\.has-.*", ".*\\.is-.*"]
|
||||
}
|
||||
"plugins": ["stylelint-selector-bem-pattern"],
|
||||
"extends": ["stylelint-config-sass-guidelines", "stylelint-config-prettier"],
|
||||
"customSyntax": "postcss-scss",
|
||||
"rules": {
|
||||
"selector-class-pattern": null,
|
||||
"selector-no-qualifying-type": [
|
||||
true,
|
||||
{
|
||||
"ignore": ["attribute", "class"]
|
||||
}
|
||||
],
|
||||
"max-nesting-depth": 4,
|
||||
"plugin/selector-bem-pattern": {
|
||||
"preset": "bem",
|
||||
"componentName": "(([a-z0-9]+(?!-$)-?)+)",
|
||||
"componentSelectors": {
|
||||
"initial": "\\.{componentName}(((__|--)(([a-z0-9\\[\\]'=]+(?!-$)-?)+))+)?$"
|
||||
},
|
||||
"ignoreSelectors": [".*\\.has-.*", ".*\\.is-.*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
language: node_js
|
||||
node_js: lts/*
|
||||
|
||||
script:
|
||||
- bash .travis/prevent-base-master.sh
|
||||
- bash .travis/omit-dist.sh
|
||||
- npm run lint
|
||||
- npm run build
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/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
|
||||
1275
CHANGELOG.md
Normal file
1275
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,15 +7,17 @@ We welcome bug reports, feature requests and pull requests. If you want to help
|
||||
Before asking questions, read our [documentation](https://github.com/sampotts/plyr) and [FAQ](https://github.com/sampotts/plyr/wiki/FAQ).
|
||||
|
||||
If these doesn't answer your question
|
||||
* Use [Stack Overflow](https://stackoverflow.com/) for questions that doesn't directly involve Plyr. This includes for example how to use Javascript, CSS or HTML5 media in general, and how to use other frameworks, libraries and technology.
|
||||
* Use [our Slack](https://bit.ly/plyr-chat) if you need help using Plyr or have questions about Plyr.
|
||||
|
||||
- Use [Stack Overflow](https://stackoverflow.com/) for questions that doesn't directly involve Plyr. This includes for example how to use Javascript, CSS or HTML5 media in general, and how to use other frameworks, libraries and technology.
|
||||
- Use [our Slack](https://bit.ly/plyr-chat) if you need help using Plyr or have questions about Plyr.
|
||||
|
||||
## Commenting
|
||||
|
||||
When commenting, keep a civil tone and stay on topic. Don't ask for [support](#support), 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.
|
||||
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.
|
||||
|
||||
## Creating issues
|
||||
|
||||
@@ -23,18 +25,30 @@ Please follow the instructions in our issue templates. Don't use github issues t
|
||||
|
||||
## Contributing features and documentation
|
||||
|
||||
* If you want to add a feature or make critical changes, you may want to ensure that this is something we also want (so you don't waste your time). Ask us about this in the corresponding issue if there is one, or on [our Slack](https://bit.ly/plyr-chat) otherwise.
|
||||
- If you want to add a feature or make critical changes, you may want to ensure that this is something we also want (so you don't waste your time). Ask us about this in the corresponding issue if there is one, or on [our Slack](https://bit.ly/plyr-chat) otherwise.
|
||||
|
||||
* Fork Plyr, and create a new branch in your fork, based on the **develop** branch
|
||||
- Fork Plyr, and create a new branch in your fork, based on the **develop** branch
|
||||
|
||||
* To test locally, you can use the demo. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build while you are working, and run a local server from the repository root directory. If you have Python installed, this command should work: `python -m SimpleHTTPServer 8080`. Then go to `http://localhost:8080/demo/`
|
||||
- To test locally, you can use the demo site. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build and it will run a local web server for development and watch for any changes.
|
||||
|
||||
* Develop and test your modifications.
|
||||
### Online one-click setup for contributing
|
||||
|
||||
* 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.
|
||||
You can use Gitpod (a free online VS Code-like IDE) for contributing. With a single click it will launch a workspace and automatically:
|
||||
|
||||
* If your modifications changes the documented behavior or add new features, document these changes in readme.md.
|
||||
- clone the plyr repo.
|
||||
- install the dependencies with `yarn install` in root directory and "demo" directory.
|
||||
- run `gulp` in root directory to start the dev server.
|
||||
|
||||
* When finished, push the changes to your GitHub repository and send a pull request to **develop**. Describe what your PR does.
|
||||
So that you can start straight away.
|
||||
|
||||
* If the Travis build fails, or if you get a code review with change requests, you can fix these by pushing new or rebased commits to the branch.
|
||||
[](https://gitpod.io/from-referrer/)
|
||||
|
||||
- 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 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](README.md).
|
||||
|
||||
- When finished, push the changes to your GitHub repository and send a pull request. Describe what your PR does.
|
||||
|
||||
- If the Travis build fails, or if you get a code review with change requests, you can fix these by pushing new or rebased commits to the branch.
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
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)
|
||||
- `Element` with the controls
|
||||
- `String` containing the desired HTML
|
||||
- `false` (or empty string or array) to disable all controls
|
||||
- `Function` that will be executed and should return one of the above
|
||||
- `Array` of options (this builds the default controls based on your choices)
|
||||
- `Element` with the controls
|
||||
- `String` containing the desired HTML
|
||||
- `false` (or empty string or array) to disable all controls
|
||||
- `Function` that will be executed and should return one of the above
|
||||
|
||||
## Using default controls
|
||||
|
||||
@@ -14,28 +14,28 @@ If you want to use the standard controls as they are, you don't need to pass any
|
||||
|
||||
```javascript
|
||||
controls: [
|
||||
'play-large', // The large play button in the center
|
||||
'restart', // Restart playback
|
||||
'rewind', // Rewind by the seek time (default 10 seconds)
|
||||
'play', // Play/pause playback
|
||||
'fast-forward', // Fast forward by the seek time (default 10 seconds)
|
||||
'progress', // The progress bar and scrubber for playback and buffering
|
||||
'current-time', // The current time of playback
|
||||
'duration', // The full duration of the media
|
||||
'mute', // Toggle mute
|
||||
'volume', // Volume control
|
||||
'captions', // Toggle captions
|
||||
'settings', // Settings menu
|
||||
'pip', // Picture-in-picture (currently Safari only)
|
||||
'airplay', // Airplay (currently Safari only)
|
||||
'download', // Show a download button with a link to either the current source or a custom URL you specify in your options
|
||||
'fullscreen', // Toggle fullscreen
|
||||
'play-large', // The large play button in the center
|
||||
'restart', // Restart playback
|
||||
'rewind', // Rewind by the seek time (default 10 seconds)
|
||||
'play', // Play/pause playback
|
||||
'fast-forward', // Fast forward by the seek time (default 10 seconds)
|
||||
'progress', // The progress bar and scrubber for playback and buffering
|
||||
'current-time', // The current time of playback
|
||||
'duration', // The full duration of the media
|
||||
'mute', // Toggle mute
|
||||
'volume', // Volume control
|
||||
'captions', // Toggle captions
|
||||
'settings', // Settings menu
|
||||
'pip', // Picture-in-picture (currently Safari only)
|
||||
'airplay', // Airplay (currently Safari only)
|
||||
'download', // Show a download button with a link to either the current source or a custom URL you specify in your options
|
||||
'fullscreen', // Toggle fullscreen
|
||||
];
|
||||
```
|
||||
|
||||
### Internationalization using default controls
|
||||
|
||||
You can provide an `i18n` object as one of your options when initialising the plugin which we be used when rendering the controls.
|
||||
You can provide an `i18n` object as one of your options when initializing the plugin which we be used when rendering the controls.
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -84,14 +84,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:
|
||||
|
||||
- `{id}` - the dynamically generated ID for the player (for form controls)
|
||||
- `{seektime}` - the seek time specified in options for fast forward and rewind
|
||||
- `{title}` - the title of your media, if specified
|
||||
- `{id}` - the dynamically generated ID for the player (for form controls)
|
||||
- `{seektime}` - the seek time specified in options for fast forward and rewind
|
||||
- `{title}` - the title of your media, if specified
|
||||
|
||||
### Limitations
|
||||
|
||||
- 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
|
||||
- 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
|
||||
|
||||
### Example
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
| 🎉 | [Plyr is merging into Vidstack](https://github.com/sampotts/plyr/issues/2408) | 🎉 |
|
||||
| :-: | :---------------------------------------------------------------------------: | :-- |
|
||||
|
||||
Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vimeo media player that supports [_modern_](#browser-support) browsers.
|
||||
|
||||
[Checkout the demo](https://plyr.io) - [Donate](#donate) - [Slack](https://bit.ly/plyr--chat) - [](https://badge.fury.io/js/plyr)
|
||||
[Checkout the demo](https://plyr.io) - [Donate](#donate) - [Slack](https://bit.ly/plyr--chat)
|
||||
|
||||
[](https://badge.fury.io/js/plyr) [](https://gitpod.io/#https://github.com/sampotts/plyr) [](https://opencollective.com/plyr)
|
||||
|
||||
[](https://plyr.io)
|
||||
|
||||
# Features
|
||||
|
||||
- 📼 **HTML Video & Audio, YouTube & Vimeo** - support for the major formats
|
||||
- 💪 **Accessible** - full support for VTT captions and screen readers
|
||||
- 🔧 **[Customizable](#html)** - make the player look how you want with the markup you want
|
||||
- 😎 **Clean HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
|
||||
`<span>` or `<a href="#">` button hacks
|
||||
- 📱 **Responsive** - works with any screen size
|
||||
- 💵 **[Monetization](#ads)** - make money from your videos
|
||||
- 📹 **[Streaming](#demos)** - support for hls.js, Shaka and dash.js streaming playback
|
||||
- 🎛 **[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
|
||||
- 🔎 **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
|
||||
- ⌨️ **[Shortcuts](#shortcuts)** - supports keyboard shortcuts
|
||||
- 🖥 **Picture-in-Picture** - supports picture-in-picture mode
|
||||
- 📱 **Playsinline** - supports the `playsinline` attribute
|
||||
- 🏎 **Speed controls** - adjust speed on the fly
|
||||
- 📖 **Multiple captions** - support for multiple caption tracks
|
||||
- 🌎 **i18n support** - support for internationalization of controls
|
||||
- 👌 **[Preview thumbnails](#preview-thumbnails)** - support for displaying preview thumbnails
|
||||
- 🤟 **No frameworks** - written in "vanilla" ES6 JavaScript, no jQuery required
|
||||
- 💁♀️ **SASS** - to include in your build processes
|
||||
- 📼 **HTML Video & Audio, YouTube & Vimeo** - support for the major formats
|
||||
- 💪 **Accessible** - full support for VTT captions and screen readers
|
||||
- 🔧 **[Customizable](#html)** - make the player look how you want with the markup you want
|
||||
- 😎 **Clean HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
|
||||
`<span>` or `<a href="#">` button hacks
|
||||
- 📱 **Responsive** - works with any screen size
|
||||
- 💵 **[Monetization](#ads)** - make money from your videos
|
||||
- 📹 **[Streaming](#demos)** - support for hls.js, Shaka and dash.js streaming playback
|
||||
- 🎛 **[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
|
||||
- 🔎 **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
|
||||
- ⌨️ **[Shortcuts](#shortcuts)** - supports keyboard shortcuts
|
||||
- 🖥 **Picture-in-Picture** - supports picture-in-picture mode
|
||||
- 📱 **Playsinline** - supports the `playsinline` attribute
|
||||
- 🏎 **Speed controls** - adjust speed on the fly
|
||||
- 📖 **Multiple captions** - support for multiple caption tracks
|
||||
- 🌎 **i18n support** - support for internationalization of controls
|
||||
- 👌 **[Preview thumbnails](#preview-thumbnails)** - support for displaying preview thumbnails
|
||||
- 🤟 **No frameworks** - written in "vanilla" ES6 JavaScript, no jQuery required
|
||||
- 💁♀️ **Sass** - to include in your build processes
|
||||
|
||||
### Demos
|
||||
|
||||
You can try Plyr in Codepen using our minimal templates: [HTML5 video](https://codepen.io/pen?template=bKeqpr), [HTML5 audio](https://codepen.io/pen?template=rKLywR), [YouTube](https://codepen.io/pen?template=GGqbbJ), [Vimeo](https://codepen.io/pen?template=bKeXNq). For Streaming we also have example integrations with: [Dash.js](https://codepen.io/pen?template=zaBgBy), [Hls.js](https://codepen.io/pen?template=oyLKQb) and [Shaka Player](https://codepen.io/pen?template=ZRpzZO)
|
||||
You can try Plyr in Codepen using our minimal templates: [HTML5 video](https://codepen.io/pen?template=bKeqpr), [HTML5 audio](https://codepen.io/pen?template=rKLywR), [YouTube](https://codepen.io/pen?template=GGqbbJ), [Vimeo](https://codepen.io/pen?template=bKeXNq). For Streaming we also have example integrations with: [Dash.js](https://codepen.io/pen?template=GRoogML), [Hls.js](https://codepen.io/pen?template=oyLKQb) and [Shaka Player](https://codepen.io/pen?template=ZRpzZO)
|
||||
|
||||
# Quick setup
|
||||
|
||||
@@ -40,21 +45,23 @@ Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.o
|
||||
### HTML5 Video
|
||||
|
||||
```html
|
||||
<video poster="/path/to/poster.jpg" id="player" playsinline controls>
|
||||
<source src="/path/to/video.mp4" type="video/mp4" />
|
||||
<source src="/path/to/video.webm" type="video/webm" />
|
||||
<video id="player" playsinline controls data-poster="/path/to/poster.jpg">
|
||||
<source src="/path/to/video.mp4" type="video/mp4" />
|
||||
<source src="/path/to/video.webm" type="video/webm" />
|
||||
|
||||
<!-- Captions are optional -->
|
||||
<track kind="captions" label="English captions" src="/path/to/captions.vtt" srclang="en" default />
|
||||
<!-- Captions are optional -->
|
||||
<track kind="captions" label="English captions" src="/path/to/captions.vtt" srclang="en" default />
|
||||
</video>
|
||||
```
|
||||
|
||||
**Note**: The poster image should be specified using `data-poster`. This is to prevent it [being downloaded twice](https://github.com/sampotts/plyr/issues/1531). If you're sure the image will be cached, you can still use the `poster` attribute for true progressive enhancement.
|
||||
|
||||
### HTML5 Audio
|
||||
|
||||
```html
|
||||
<audio id="player" controls>
|
||||
<source src="/path/to/audio.mp3" type="audio/mp3" />
|
||||
<source src="/path/to/audio.ogg" type="audio/ogg" />
|
||||
<source src="/path/to/audio.mp3" type="audio/mp3" />
|
||||
<source src="/path/to/audio.ogg" type="audio/ogg" />
|
||||
</audio>
|
||||
```
|
||||
|
||||
@@ -66,12 +73,12 @@ We recommend [progressive enhancement](https://www.smashingmagazine.com/2009/04/
|
||||
|
||||
```html
|
||||
<div class="plyr__video-embed" id="player">
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/bTqVqk7FSmY?origin=https://plyr.io&iv_load_policy=3&modestbranding=1&playsinline=1&showinfo=0&rel=0&enablejsapi=1"
|
||||
allowfullscreen
|
||||
allowtransparency
|
||||
allow="autoplay"
|
||||
></iframe>
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/bTqVqk7FSmY?origin=https://plyr.io&iv_load_policy=3&modestbranding=1&playsinline=1&showinfo=0&rel=0&enablejsapi=1"
|
||||
allowfullscreen
|
||||
allowtransparency
|
||||
allow="autoplay"
|
||||
></iframe>
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -91,12 +98,12 @@ Much the same as YouTube above.
|
||||
|
||||
```html
|
||||
<div class="plyr__video-embed" id="player">
|
||||
<iframe
|
||||
src="https://player.vimeo.com/video/76979871?loop=false&byline=false&portrait=false&title=false&speed=true&transparent=0&gesture=media"
|
||||
allowfullscreen
|
||||
allowtransparency
|
||||
allow="autoplay"
|
||||
></iframe>
|
||||
<iframe
|
||||
src="https://player.vimeo.com/video/76979871?loop=false&byline=false&portrait=false&title=false&speed=true&transparent=0&gesture=media"
|
||||
allowfullscreen
|
||||
allowtransparency
|
||||
allow="autoplay"
|
||||
></iframe>
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -121,27 +128,27 @@ Alternatively you can include the `plyr.js` script before the closing `</body>`
|
||||
```html
|
||||
<script src="path/to/plyr.js"></script>
|
||||
<script>
|
||||
const player = new Plyr('#player');
|
||||
const player = new Plyr('#player');
|
||||
</script>
|
||||
```
|
||||
|
||||
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 separately as part of your application but to make life easier you can use the polyfilled build.
|
||||
|
||||
```html
|
||||
<script src="https://cdn.plyr.io/3.5.10/plyr.js"></script>
|
||||
<script src="https://cdn.plyr.io/3.7.0/plyr.js"></script>
|
||||
```
|
||||
|
||||
...or...
|
||||
|
||||
```html
|
||||
<script src="https://cdn.plyr.io/3.5.10/plyr.polyfilled.js"></script>
|
||||
<script src="https://cdn.plyr.io/3.7.0/plyr.polyfilled.js"></script>
|
||||
```
|
||||
|
||||
## CSS
|
||||
|
||||
Include the `plyr.css` stylsheet into your `<head>`.
|
||||
Include the `plyr.css` stylesheet into your `<head>`.
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="path/to/plyr.css" />
|
||||
@@ -150,30 +157,126 @@ 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:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.5.10/plyr.css" />
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.0/plyr.css" />
|
||||
```
|
||||
|
||||
## 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
|
||||
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.5.10/plyr.svg`.
|
||||
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.7.0/plyr.svg`.
|
||||
|
||||
# Ads
|
||||
|
||||
Plyr has partnered up with [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) to offer monetization options for your videos. Getting setup is easy:
|
||||
|
||||
- [Sign up for a vi.ai account](https://vi.ai/publisher-video-monetization/?aid=plyrio)
|
||||
- Grab your publisher ID from the code snippet
|
||||
- Enable ads in the [config options](#options) and enter your publisher ID
|
||||
- [Sign up for a vi.ai account](https://vi.ai/publisher-video-monetization/?aid=plyrio)
|
||||
- Grab your publisher ID from the code snippet
|
||||
- Enable ads in the [config options](#options) and enter your publisher ID
|
||||
|
||||
Any questions regarding the ads can be sent straight to vi.ai and any issues with rendering raised through GitHub issues.
|
||||
|
||||
If you do not wish to use Vi, you can set your own `ads.tagUrl` [option](#options).
|
||||
|
||||
# Advanced
|
||||
|
||||
## SASS
|
||||
## Customizing the CSS
|
||||
|
||||
You can use `bundle.scss` file included in `/src` as part of your build and change variables to suit your design. The SASS require you to
|
||||
use the [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) plugin (you should be already!) as all declarations use the W3C definitions.
|
||||
If you want to change any design tokens used for the rendering of the player, you can do so using [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties).
|
||||
|
||||
Here's a list of the properties and what they are used for:
|
||||
|
||||
| Name | Description | Default / Fallback |
|
||||
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `--plyr-color-main` | The primary UI color. |  `#00b3ff` |
|
||||
| `--plyr-video-background` | The background color of video and poster wrappers for using alpha channel videos and poster images. | `rgba(0, 0, 0, 1)` |
|
||||
| `--plyr-tab-focus-color` | The color used for the dotted outline when an element is `:focus-visible` (equivalent) keyboard focus. | `--plyr-color-main` |
|
||||
| `--plyr-badge-background` | The background color for badges in the menu. |  `#4a5464` |
|
||||
| `--plyr-badge-text-color` | The text color for badges. |  `#ffffff` |
|
||||
| `--plyr-badge-border-radius` | The border radius used for badges. | `2px` |
|
||||
| `--plyr-tab-focus-color` | The color used to highlight tab (keyboard) focus. | `--plyr-color-main` |
|
||||
| `--plyr-captions-background` | The color for the background of captions. | `rgba(0, 0, 0, 0.8)` |
|
||||
| `--plyr-captions-text-color` | The color used for the captions text. |  `#ffffff` |
|
||||
| `--plyr-control-icon-size` | The size of the icons used in the controls. | `18px` |
|
||||
| `--plyr-control-spacing` | The space between controls (sometimes used in a multiple - e.g. `10px / 2 = 5px`). | `10px` |
|
||||
| `--plyr-control-padding` | The padding inside controls. | `--plyr-control-spacing * 0.7` (`7px`) |
|
||||
| `--plyr-control-radius` | The border radius used on controls. | `3px` |
|
||||
| `--plyr-control-toggle-checked-background` | The background color used for checked menu items. | `--plyr-color-main` |
|
||||
| `--plyr-video-controls-background` | The background for the video controls. | `linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75))` |
|
||||
| `--plyr-video-control-color` | The text/icon color for video controls. |  `#ffffff` |
|
||||
| `--plyr-video-control-color-hover` | The text/icon color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). |  `#ffffff` |
|
||||
| `--plyr-video-control-background-hover` | The background color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | `--plyr-color-main` |
|
||||
| `--plyr-audio-controls-background` | The background for the audio controls. |  `#ffffff` |
|
||||
| `--plyr-audio-control-color` | The text/icon color for audio controls. |  `#4a5464` |
|
||||
| `--plyr-audio-control-color-hover` | The text/icon color used when audio controls are `:hover`, `:focus` and `:focus-visible` (equivalent). |  `#ffffff` |
|
||||
| `--plyr-audio-control-background-hover` | The background color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | `--plyr-color-main` |
|
||||
| `--plyr-menu-background` | The background color for menus. | `rgba(255, 255, 255, 0.9)` |
|
||||
| `--plyr-menu-color` | The text/icon color for menu items. |  `#4a5464` |
|
||||
| `--plyr-menu-shadow` | The shadow used on menus. | `0 1px 2px rgba(0, 0, 0, 0.15)` |
|
||||
| `--plyr-menu-radius` | The border radius on the menu. | `4px` |
|
||||
| `--plyr-menu-arrow-size` | The size of the arrow on the bottom of the menu. | `6px` |
|
||||
| `--plyr-menu-item-arrow-color` | The color of the arrows in the menu. |  `#728197` |
|
||||
| `--plyr-menu-item-arrow-size` | The size of the arrows in the menu. | `4px` |
|
||||
| `--plyr-menu-border-color` | The border color for the bottom of the back button in the top of the sub menu pages. |  `#dcdfe5` |
|
||||
| `--plyr-menu-border-shadow-color` | The shadow below the border of the back button in the top of the sub menu pages. |  `#ffffff` |
|
||||
| `--plyr-progress-loading-size` | The size of the stripes in the loading state in the scrubber. | `25px` |
|
||||
| `--plyr-progress-loading-background` | The background color on the loading state in the scrubber. | `rgba(35, 40, 47, 0.6)` |
|
||||
| `--plyr-video-progress-buffered-background` | The fill color for the buffer indication in the scrubber for video. | `rgba(255, 255, 255, 0.25)` |
|
||||
| `--plyr-audio-progress-buffered-background` | The fill color for the buffer indication in the scrubber for audio. | `rgba(193, 200, 209, 0.6)` |
|
||||
| `--plyr-range-thumb-height` | The height of the scrubber handle/thumb. | `13px` |
|
||||
| `--plyr-range-thumb-background` | The background of the scrubber handle/thumb. |  `#ffffff` |
|
||||
| `--plyr-range-thumb-shadow` | The shadow of the scrubber handle/thumb. | `0 1px 1px rgba(215, 26, 18, 0.15), 0 0 0 1px rgba(215, 26, 18, 0.2)` |
|
||||
| `--plyr-range-thumb-active-shadow-width` | The width of the shadow when the scrubber handle/thumb is `:active` (pressed). | `3px` |
|
||||
| `--plyr-range-track-height` | The height of the scrubber/progress track. | `5px` |
|
||||
| `--plyr-range-fill-background` | The fill color of the scrubber/progress. | `--plyr-color-main` |
|
||||
| `--plyr-video-range-track-background` | The background of the scrubber/progress. | `--plyr-video-progress-buffered-background` |
|
||||
| `--plyr-video-range-thumb-active-shadow-color` | The color of the shadow when the video scrubber handle/thumb is `:active` (pressed). | `rgba(255, 255, 255, 0.5)` |
|
||||
| `--plyr-audio-range-track-background` | The background of the scrubber/progress. | `--plyr-video-progress-buffered-background` |
|
||||
| `--plyr-audio-range-thumb-active-shadow-color` | The color of the shadow when the audio scrubber handle/thumb is `:active` (pressed). | `rgba(215, 26, 18, 0.1)` |
|
||||
| `--plyr-tooltip-background` | The background color for tooltips. | `rgba(255, 255, 255, 0.9)` |
|
||||
| `--plyr-tooltip-color` | The text color for tooltips. |  `#4a5464` |
|
||||
| `--plyr-tooltip-padding` | The padding for tooltips. | `calc(var(--plyr-control-spacing) / 2))` |
|
||||
| `--plyr-tooltip-arrow-size` | The size of the arrow under tooltips. | `4px` |
|
||||
| `--plyr-tooltip-radius` | The border radius on tooltips. | `3px` |
|
||||
| `--plyr-tooltip-shadow` | The shadow on tooltips. | `0 1px 2px rgba(0, 0, 0, 0.15)` |
|
||||
| `--plyr-font-family` | The font family used in the player. | |
|
||||
| `--plyr-font-size-base` | The base font size. Mainly used for captions. | `15px` |
|
||||
| `--plyr-font-size-small` | The smaller font size. Mainly used for captions. | `13px` |
|
||||
| `--plyr-font-size-large` | The larger font size. Mainly used for captions. | `18px` |
|
||||
| `--plyr-font-size-xlarge` | The even larger font size. Mainly used for captions. | `21px` |
|
||||
| `--plyr-font-size-time` | The font size for the time. | `--plyr-font-size-small` |
|
||||
| `--plyr-font-size-menu` | The font size used in the menu. | `--plyr-font-size-small` |
|
||||
| `--plyr-font-size-badge` | The font size used for badges. | `9px` |
|
||||
| `--plyr-font-weight-regular` | The regular font weight. | `400` |
|
||||
| `--plyr-font-weight-bold` | The bold font weight. | `600` |
|
||||
| `--plyr-line-height` | The line height used within the player. | `1.7` |
|
||||
| `--plyr-font-smoothing` | Whether to enable font antialiasing within the player. | `false` |
|
||||
|
||||
You can set them in your CSS for all players:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--plyr-color-main: #1ac266;
|
||||
}
|
||||
```
|
||||
|
||||
...or for a specific class name:
|
||||
|
||||
```css
|
||||
.player {
|
||||
--plyr-color-main: #1ac266;
|
||||
}
|
||||
```
|
||||
|
||||
...or in your HTML:
|
||||
|
||||
```html
|
||||
<video class="player" style="--plyr-color-main: #1ac266;">...</video>
|
||||
```
|
||||
|
||||
### Sass
|
||||
|
||||
You can use `plyr.scss` file included in `/src/sass` as part of your build and change variables to suit your design. The Sass requires you to
|
||||
use [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) (you should be already!) as all declarations use the W3C definitions.
|
||||
|
||||
The HTML markup uses the BEM methodology with `plyr` as the block, e.g. `.plyr__controls`. You can change the class hooks in the options to match any custom CSS
|
||||
you write. Check out the JavaScript source for more on this.
|
||||
@@ -211,9 +314,9 @@ WebVTT captions are supported. To add a caption track, check the HTML example ab
|
||||
|
||||
You can specify a range of arguments for the constructor to use:
|
||||
|
||||
- A [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)
|
||||
- A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
|
||||
- A [jQuery](https://jquery.com) object
|
||||
- A [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)
|
||||
- A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
|
||||
- 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 [multiple players](#multiple-players) below.
|
||||
|
||||
@@ -242,7 +345,7 @@ The HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<
|
||||
You have two choices here. You can either use a simple array loop to map the constructor:
|
||||
|
||||
```javascript
|
||||
const players = Array.from(document.querySelectorAll('.js-player')).map(p => new Plyr(p));
|
||||
const players = Array.from(document.querySelectorAll('.js-player')).map((p) => new Plyr(p));
|
||||
```
|
||||
|
||||
...or use a static method where you can pass a [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement), or a [JQuery](https://jquery.com) object:
|
||||
@@ -259,7 +362,7 @@ The second argument for the constructor is the [options](#options) object:
|
||||
|
||||
```javascript
|
||||
const player = new Plyr('#player', {
|
||||
title: 'Example Title',
|
||||
title: 'Example Title',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -275,7 +378,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
||||
| -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | Boolean | `true` | Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below. |
|
||||
| `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 the default controls are used, you can specify which settings to show in the menu |
|
||||
| `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. |
|
||||
@@ -298,25 +401,27 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
||||
| `invertTime` | Boolean | `true` | Display the current time as a countdown rather than an incremental counter. |
|
||||
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
|
||||
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
|
||||
| `captions` | Object | `{ active: false, language: 'auto', update: false }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). 'auto' uses the browser language. `update`: Listen to changes to tracks and update menu. This is needed for some streaming libraries, but can result in unselectable language options). |
|
||||
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution (`true`/`false`/`'force'`). `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) |
|
||||
| `captions` | Object | `{ active: false, language: 'auto', update: false }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). 'auto' uses the browser language. `update`: Listen to changes to tracks and update menu. This is needed for some streaming libraries, but can result in non-selectable language options). |
|
||||
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false, container: null }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution (`true`/`false`/`'force'`). `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls). `container`: A selector for an ancestor of the player element, allows contextual content to remain visual in fullscreen mode. Non-ancestors are ignored. |
|
||||
| `ratio` | String | `null` | Force an aspect ratio for all videos. The format is `'w:h'` - e.g. `'16:9'` or `'4:3'`. If this is not specified then the default for HTML5 and Vimeo is to use the native resolution of the video. As dimensions are not available from YouTube via SDK, 16:9 is forced as a sensible default. |
|
||||
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
|
||||
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: The speed options to display in the UI. YouTube and Vimeo will ignore any options outside of the 0.5-2 range, so options outside of this range will be hidden automatically. |
|
||||
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4] }` | `selected`: The default speed for playback. `options`: The speed options to display in the UI. YouTube and Vimeo will ignore any options outside of the 0.5-2 range, so options outside of this range will be hidden automatically. |
|
||||
| `quality` | Object | `{ default: 576, options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240] }` | `default` is the default quality level (if it exists in your sources). `options` are the options to display. This is used to filter the available sources. |
|
||||
| `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. |
|
||||
| `ads` | Object | `{ enabled: false, publisherId: '' }` | `enabled`: Whether to enable advertisements. `publisherId`: Your unique [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) publisher ID. |
|
||||
| `ads` | Object | `{ enabled: false, publisherId: '', tagUrl: '' }` | `enabled`: Whether to enable advertisements. `publisherId`: Your unique [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) publisher ID. `tagUrl` is a URL for a custom VAST tag if you're not using Vi. |
|
||||
| `urls` | Object | See source. | If you wish to override any API URLs then you can do so here. You can also set a custom download URL for the download button. |
|
||||
| `vimeo` | Object | `{ byline: false, portrait: false, title: false, speed: true, transparent: false }` | See [Vimeo embed options](https://github.com/vimeo/player.js/#embed-options). Some are set automatically based on other config options, namely: `loop`, `autoplay`, `muted`, `gesture`, `playsinline` |
|
||||
| `youtube` | Object | `{ noCookie: false, rel: 0, showinfo: 0, iv_load_policy: 3, modestbranding: 1 }` | See [YouTube embed options](https://developers.google.com/youtube/player_parameters#Parameters). The only custom option is `noCookie` to use an alternative to YouTube that doesn't use cookies (useful for GDPR, etc). Some are set automatically based on other config options, namely: `autoplay`, `hl`, `controls`, `disablekb`, `playsinline`, `cc_load_policy`, `cc_lang_pref`, `widget_referrer` |
|
||||
| `previewThumbnails` | Object | `{ enabled: false, src: '' }` | `enabled`: Whether to enable the preview thumbnails (they must be generated by you). `src` must be either a string or an array of strings representing URLs for the VTT files containing the image URL(s). Learn more about [preview thumbnails](#preview-thumbnails) below. |
|
||||
| `mediaMetadata` | Object | `{ title: '', artist: '', album: '', artwork: [] }` | The [MediaMetadata](https://developer.mozilla.org/en-US/docs/Web/API/MediaMetadata) interface of the Media Session API allows a web page to provide rich media metadata for display in a platform UI. |
|
||||
| `markers` | Object | `{ enabled: false, points: [] }` | `enabled`: Whether to enable markers. `points` is an array of `{ time: number; label: string; }` objects where `time` represents the marker position in seconds and `label` is the HTML string to be displayed. |
|
||||
|
||||
1. Vimeo only
|
||||
2. Autoplay is generally not recommended as it is seen as a negative user experience. It is also disabled in many browsers. Before raising issues, do your homework. More info can be found here:
|
||||
|
||||
- https://webkit.org/blog/6784/new-video-policies-for-ios/
|
||||
- https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
|
||||
- https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/
|
||||
- https://webkit.org/blog/6784/new-video-policies-for-ios/
|
||||
- https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
|
||||
- https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/
|
||||
|
||||
# API
|
||||
|
||||
@@ -328,15 +433,15 @@ The easiest way to access the Plyr object is to set the return value from your c
|
||||
|
||||
```javascript
|
||||
const player = new Plyr('#player', {
|
||||
/* options */
|
||||
/* options */
|
||||
});
|
||||
```
|
||||
|
||||
You can also access the object through any events:
|
||||
|
||||
```javascript
|
||||
element.addEventListener('ready', event => {
|
||||
const player = event.detail.plyr;
|
||||
element.addEventListener('ready', (event) => {
|
||||
const player = event.detail.plyr;
|
||||
});
|
||||
```
|
||||
|
||||
@@ -349,30 +454,31 @@ player.play(); // Start playback
|
||||
player.fullscreen.enter(); // Enter fullscreen
|
||||
```
|
||||
|
||||
| Method | Parameters | Description |
|
||||
| ------------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `play()`¹ | - | Start playback. |
|
||||
| `pause()` | - | Pause playback. |
|
||||
| `togglePlay(toggle)` | Boolean | Toggle playback, if no parameters are passed, it will toggle based on current status. |
|
||||
| `stop()` | - | Stop playback and reset to start. |
|
||||
| `restart()` | - | Restart playback. |
|
||||
| `rewind(seekTime)` | Number | Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used. |
|
||||
| `forward(seekTime)` | Number | Fast forward by the specified seek time. If no parameter is passed, the default seek time will be used. |
|
||||
| `increaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
|
||||
| `decreaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
|
||||
| `toggleCaptions(toggle)` | Boolean | Toggle captions display. If no parameter is passed, it will toggle based on current status. |
|
||||
| `fullscreen.enter()` | - | Enter fullscreen. If fullscreen is not supported, a fallback "full window/viewport" is used instead. |
|
||||
| `fullscreen.exit()` | - | Exit fullscreen. |
|
||||
| `fullscreen.toggle()` | - | Toggle fullscreen. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `supports(type)` | String | Check support for a mime type. |
|
||||
| `destroy()` | - | Destroy the instance and garbage collect any elements. |
|
||||
| Method | Parameters | Description |
|
||||
| -------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `play()`¹ | - | Start playback. |
|
||||
| `pause()` | - | Pause playback. |
|
||||
| `togglePlay(toggle)`¹ | Boolean | Toggle playback, if no parameters are passed, it will toggle based on current status. |
|
||||
| `stop()` | - | Stop playback and reset to start. |
|
||||
| `restart()` | - | Restart playback. |
|
||||
| `rewind(seekTime)` | Number | Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used. |
|
||||
| `forward(seekTime)` | Number | Fast forward by the specified seek time. If no parameter is passed, the default seek time will be used. |
|
||||
| `increaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
|
||||
| `decreaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
|
||||
| `toggleCaptions(toggle)` | Boolean | Toggle captions display. If no parameter is passed, it will toggle based on current status. |
|
||||
| `fullscreen.enter()` | - | Enter fullscreen. If fullscreen is not supported, a fallback "full window/viewport" is used instead. |
|
||||
| `fullscreen.exit()` | - | Exit fullscreen. |
|
||||
| `fullscreen.toggle()` | - | Toggle fullscreen. |
|
||||
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
|
||||
| `setPreviewThumbnails(source: PreviewThumbnailsOptions)` | - | Sets the preview thumbnails for the current source. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `supports(type)` | String | Check support for a mime type. |
|
||||
| `destroy()` | - | Destroy the instance and garbage collect any elements. |
|
||||
|
||||
1. For HTML5 players, `play()` will return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) in _some_ browsers - WebKit and Mozilla [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) at time of writing.
|
||||
1. For HTML5 players, `play()` will return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) for most browsers - e.g. Chrome, Firefox, Opera, Safari and Edge [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) at time of writing.
|
||||
|
||||
## Getters and Setters
|
||||
|
||||
@@ -411,6 +517,7 @@ player.fullscreen.active; // false;
|
||||
| `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](#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. |
|
||||
| `previewThumbnails` | ✓ | ✓ | Gets or sets the current preview thumbnail source for the player. The setter accepts a string |
|
||||
| `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 |
|
||||
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. If your captions don't have any language data, or if you have multiple tracks with the same language, you may want to use `currentTrack` instead. |
|
||||
@@ -430,39 +537,39 @@ Video example:
|
||||
|
||||
```javascript
|
||||
player.source = {
|
||||
type: 'video',
|
||||
title: 'Example title',
|
||||
sources: [
|
||||
{
|
||||
src: '/path/to/movie.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 720,
|
||||
},
|
||||
{
|
||||
src: '/path/to/movie.webm',
|
||||
type: 'video/webm',
|
||||
size: 1080,
|
||||
},
|
||||
],
|
||||
poster: '/path/to/poster.jpg',
|
||||
previewThumbnails: {
|
||||
src: '/path/to/thumbnails.vtt'
|
||||
type: 'video',
|
||||
title: 'Example title',
|
||||
sources: [
|
||||
{
|
||||
src: '/path/to/movie.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 720,
|
||||
},
|
||||
tracks: [
|
||||
{
|
||||
kind: 'captions',
|
||||
label: 'English',
|
||||
srclang: 'en',
|
||||
src: '/path/to/captions.en.vtt',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
kind: 'captions',
|
||||
label: 'French',
|
||||
srclang: 'fr',
|
||||
src: '/path/to/captions.fr.vtt',
|
||||
},
|
||||
],
|
||||
{
|
||||
src: '/path/to/movie.webm',
|
||||
type: 'video/webm',
|
||||
size: 1080,
|
||||
},
|
||||
],
|
||||
poster: '/path/to/poster.jpg',
|
||||
previewThumbnails: {
|
||||
src: '/path/to/thumbnails.vtt',
|
||||
},
|
||||
tracks: [
|
||||
{
|
||||
kind: 'captions',
|
||||
label: 'English',
|
||||
srclang: 'en',
|
||||
src: '/path/to/captions.en.vtt',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
kind: 'captions',
|
||||
label: 'French',
|
||||
srclang: 'fr',
|
||||
src: '/path/to/captions.fr.vtt',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
@@ -470,18 +577,18 @@ Audio example:
|
||||
|
||||
```javascript
|
||||
player.source = {
|
||||
type: 'audio',
|
||||
title: 'Example title',
|
||||
sources: [
|
||||
{
|
||||
src: '/path/to/audio.mp3',
|
||||
type: 'audio/mp3',
|
||||
},
|
||||
{
|
||||
src: '/path/to/audio.ogg',
|
||||
type: 'audio/ogg',
|
||||
},
|
||||
],
|
||||
type: 'audio',
|
||||
title: 'Example title',
|
||||
sources: [
|
||||
{
|
||||
src: '/path/to/audio.mp3',
|
||||
type: 'audio/mp3',
|
||||
},
|
||||
{
|
||||
src: '/path/to/audio.ogg',
|
||||
type: 'audio/ogg',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
@@ -489,29 +596,27 @@ YouTube example:
|
||||
|
||||
```javascript
|
||||
player.source = {
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: 'bTqVqk7FSmY',
|
||||
provider: 'youtube',
|
||||
},
|
||||
],
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: 'bTqVqk7FSmY',
|
||||
provider: 'youtube',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
_Note_: `src` can be the video ID or URL
|
||||
|
||||
Vimeo example
|
||||
|
||||
```javascript
|
||||
player.source = {
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: '143418951',
|
||||
provider: 'vimeo',
|
||||
},
|
||||
],
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: '143418951',
|
||||
provider: 'vimeo',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
@@ -535,8 +640,8 @@ reference to the instance, you can use the `on()` API method or `addEventListene
|
||||
property. Here's an example:
|
||||
|
||||
```javascript
|
||||
player.on('ready', event => {
|
||||
const instance = event.detail.plyr;
|
||||
player.on('ready', (event) => {
|
||||
const instance = event.detail.plyr;
|
||||
});
|
||||
```
|
||||
|
||||
@@ -595,8 +700,8 @@ YouTube and Vimeo are currently supported and function much like a HTML5 video.
|
||||
to access the API's directly. You can do so via the `embed` property of your player object - e.g. `player.embed`. You can then use the relevant methods from the
|
||||
third party APIs. More info on the respective API's here:
|
||||
|
||||
- [YouTube iframe API Reference](https://developers.google.com/youtube/iframe_api_reference)
|
||||
- [Vimeo player.js Reference](https://github.com/vimeo/player.js)
|
||||
- [YouTube iframe API Reference](https://developers.google.com/youtube/iframe_api_reference)
|
||||
- [Vimeo player.js Reference](https://github.com/vimeo/player.js)
|
||||
|
||||
_Note_: Not all API methods may work 100%. Your mileage may vary. It's better to use the Plyr API where possible.
|
||||
|
||||
@@ -662,9 +767,9 @@ const supported = Plyr.supported('video', 'html5', true);
|
||||
|
||||
The arguments are:
|
||||
|
||||
- Media type (`audio` or `video`)
|
||||
- Provider (`html5`, `youtube` or `vimeo`)
|
||||
- Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
|
||||
- Media type (`audio` or `video`)
|
||||
- Provider (`html5`, `youtube` or `vimeo`)
|
||||
- Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
|
||||
|
||||
## Disable support programmatically
|
||||
|
||||
@@ -672,7 +777,7 @@ The `enabled` option can be used to disable certain User Agents. For example, if
|
||||
|
||||
```javascript
|
||||
{
|
||||
enabled: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
enabled: !/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -682,15 +787,16 @@ If a User Agent is disabled but supports `<video>` and `<audio>` natively, it wi
|
||||
|
||||
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
|
||||
|
||||
| Type | Maintainer | Link |
|
||||
| --------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
|
||||
| Angular | Simon Bobrov ([@smnbbrv](https://github.com/smnbbrv)) | [https://github.com/smnbbrv/ngx-plyr](https://github.com/smnbbrv/ngx-plyr) |
|
||||
| React | Chintan Prajapati ([@chintan9](https://github.com/chintan9)) | [https://github.com/chintan9/plyr-react](https://github.com/chintan9/plyr-react) |
|
||||
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
|
||||
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
|
||||
| Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
|
||||
| REDAXO | FriendsOfRedaxo / skerbis ([@skerbis](https://friendsofredaxo.github.io)) | [https://github.com/FriendsOfREDAXO/plyr](https://github.com/FriendsOfREDAXO/plyr) |
|
||||
| Type | Maintainer | Link |
|
||||
| ----------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
|
||||
| Angular | Simon Bobrov ([@smnbbrv](https://github.com/smnbbrv)) | [https://github.com/smnbbrv/ngx-plyr](https://github.com/smnbbrv/ngx-plyr) |
|
||||
| React | Chintan Prajapati ([@chintan9](https://github.com/chintan9)) | [https://github.com/chintan9/plyr-react](https://github.com/chintan9/plyr-react) |
|
||||
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
|
||||
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
|
||||
| Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
|
||||
| REDAXO | FriendsOfRedaxo / skerbis ([@skerbis](https://friendsofredaxo.github.io)) | [https://github.com/FriendsOfREDAXO/plyr](https://github.com/FriendsOfREDAXO/plyr) |
|
||||
| svelte-plyr | Ben Woodward / benwoodward ([@benwoodward](https://github.com/benwoodward)) | [https://github.com/benwoodward/svelte-plyr](https://github.com/benwoodward/svelte-plyr) |
|
||||
|
||||
# Issues
|
||||
|
||||
@@ -705,42 +811,47 @@ Plyr is developed by [@sam_potts](https://twitter.com/sam_potts) / [sampotts.me]
|
||||
|
||||
Plyr costs money to run, not only my time. I donate my time for free as I enjoy building Plyr but unfortunately have to pay for domains, hosting, and more. Any help with costs is appreciated...
|
||||
|
||||
- [Donate via Patreon](https://www.patreon.com/plyr)
|
||||
- [Donate via PayPal](https://www.paypal.me/pottsy/20usd)
|
||||
- [Donate via Patreon](https://www.patreon.com/plyr)
|
||||
- [Donate via PayPal](https://www.paypal.me/pottsy/20usd)
|
||||
|
||||
# Mentions
|
||||
|
||||
- [ProductHunt](https://www.producthunt.com/tech/plyr)
|
||||
- [The Changelog](http://thechangelog.com/plyr-simple-html5-media-player-custom-controls-webvtt-captions/)
|
||||
- [HTML5 Weekly #177](http://html5weekly.com/issues/177)
|
||||
- [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f)
|
||||
- [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/)
|
||||
- [Front End Focus #177](https://frontendfoc.us/issues/177)
|
||||
- [Hacker News](https://news.ycombinator.com/item?id=9136774)
|
||||
- [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04)
|
||||
- [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player)
|
||||
- [The Treehouse Show #131](https://teamtreehouse.com/library/episode-131-origami-react-responsive-hero-images)
|
||||
- [noupe.com](http://www.noupe.com/design/html5-plyr-is-a-responsive-and-accessible-video-player-94389.html)
|
||||
- [ProductHunt](https://www.producthunt.com/tech/plyr)
|
||||
- [The Changelog](http://thechangelog.com/plyr-simple-html5-media-player-custom-controls-webvtt-captions/)
|
||||
- [HTML5 Weekly #177](http://html5weekly.com/issues/177)
|
||||
- [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f)
|
||||
- [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/)
|
||||
- [Front End Focus #177](https://frontendfoc.us/issues/177)
|
||||
- [Hacker News](https://news.ycombinator.com/item?id=9136774)
|
||||
- [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04)
|
||||
- [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player)
|
||||
- [The Treehouse Show #131](https://teamtreehouse.com/library/episode-131-origami-react-responsive-hero-images)
|
||||
- [noupe.com](http://www.noupe.com/design/html5-plyr-is-a-responsive-and-accessible-video-player-94389.html)
|
||||
|
||||
# Used by
|
||||
|
||||
- [Selz.com](https://selz.com)
|
||||
- [Peugeot.fr](http://www.peugeot.fr/marque-et-technologie/technologies/peugeot-i-cockpit.html)
|
||||
- [Peugeot.de](http://www.peugeot.de/modelle/modellberater/208-3-turer/fotos-videos.html)
|
||||
- [TomTom.com](http://prioritydriving.tomtom.com/)
|
||||
- [DIGBMX](http://digbmx.com/)
|
||||
- [Grime Archive](https://grimearchive.com/)
|
||||
- [koel - A personal music streaming server that works.](http://koel.phanan.net/)
|
||||
- [Oscar Radio](http://oscar-radio.xyz/)
|
||||
- [Sparkk TV](https://www.sparkktv.com/)
|
||||
- [@halfhalftravel](https://www.halfhalftravel.com/)
|
||||
- [Selz.com](https://selz.com)
|
||||
- [Peugeot.fr](http://www.peugeot.fr/marque-et-technologie/technologies/peugeot-i-cockpit.html)
|
||||
- [Peugeot.de](http://www.peugeot.de/modelle/modellberater/208-3-turer/fotos-videos.html)
|
||||
- [TomTom.com](http://prioritydriving.tomtom.com/)
|
||||
- [DIGBMX](http://digbmx.com/)
|
||||
- [Grime Archive](https://grimearchive.com/)
|
||||
- [koel - A personal music streaming server that works.](http://koel.phanan.net/)
|
||||
- [Oscar Radio](http://oscar-radio.xyz/)
|
||||
- [Sparkk TV](https://www.sparkktv.com/)
|
||||
- [@halfhalftravel](https://www.halfhalftravel.com/)
|
||||
- [BitChute](https://www.bitchute.com)
|
||||
- [Rutheneum-Bote](https://gymnasium-rutheneum.de/content/newspaper/kreativwettbewerb.php)
|
||||
- [pressakey.com | Blog-Magazin für Videospiele](https://pressakey.com)
|
||||
- [STROLLÿN: Work with a View](https://strollyn.com)
|
||||
- [CFDA Runway360](https://runway360.cfda.com/)
|
||||
|
||||
If you want to be added to the list, open a pull request. It'd be awesome to see how you're using Plyr 😎
|
||||
|
||||
# Useful links and credits
|
||||
|
||||
- [PayPal's Accessible HTML5 Video Player (which Plyr was originally ported from)](https://github.com/paypal/accessible-html5-video-player)
|
||||
- [An awesome guide for Plyr in Japanese!](http://syncer.jp/how-to-use-plyr-io) by [@arayutw](https://twitter.com/arayutw)
|
||||
- [PayPal's Accessible HTML5 Video Player (which Plyr was originally ported from)](https://github.com/paypal/accessible-html5-video-player)
|
||||
- [An awesome guide for Plyr in Japanese!](http://syncer.jp/how-to-use-plyr-io) by [@arayutw](https://twitter.com/arayutw)
|
||||
|
||||
# Thanks
|
||||
|
||||
@@ -752,6 +863,29 @@ Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN servic
|
||||
|
||||
Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site.
|
||||
|
||||
## Contributors
|
||||
|
||||
### Code Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
|
||||
|
||||
<a href="https://github.com/sampotts/plyr/graphs/contributors"><img src="https://opencollective.com/plyr/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
### Financial Contributors
|
||||
|
||||
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/plyr/contribute)]
|
||||
|
||||
#### Individuals
|
||||
|
||||
<a href="https://opencollective.com/plyr"><img src="https://opencollective.com/plyr/individuals.svg?width=890"></a>
|
||||
|
||||
#### Organizations
|
||||
|
||||
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/plyr/contribute)]
|
||||
|
||||
<a href="https://opencollective.com/plyr/organization/0/website"><img src="https://opencollective.com/plyr/organization/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/plyr/organization/1/website"><img src="https://opencollective.com/plyr/organization/1/avatar.svg"></a><a href="https://opencollective.com/plyr/organization/2/website"><img src="https://opencollective.com/plyr/organization/2/avatar.svg"></a>
|
||||
|
||||
# Copyright and License
|
||||
|
||||
[The MIT license](license.md)
|
||||
[The MIT license](LICENSE.md)
|
||||
1153
changelog.md
1153
changelog.md
File diff suppressed because it is too large
Load Diff
34
cspell.json
Normal file
34
cspell.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"version": "0.2",
|
||||
"ignorePaths": ["package.json", "dist/*", "demo/node_modules/*"],
|
||||
"dictionaryDefinitions": [],
|
||||
"dictionaries": [],
|
||||
"words": [
|
||||
"autopause",
|
||||
"autoplay",
|
||||
"bote",
|
||||
"cfda",
|
||||
"classname",
|
||||
"digbmx",
|
||||
"fastly",
|
||||
"fullscreen",
|
||||
"gordita",
|
||||
"magazin",
|
||||
"menuitemradio",
|
||||
"noupe",
|
||||
"playsinline",
|
||||
"plyr",
|
||||
"rutheneum",
|
||||
"seektime",
|
||||
"selz",
|
||||
"sparkk",
|
||||
"srclang",
|
||||
"strol",
|
||||
"stylelint",
|
||||
"unmute",
|
||||
"videospiele",
|
||||
"xywh"
|
||||
],
|
||||
"ignoreWords": [],
|
||||
"import": []
|
||||
}
|
||||
1
demo/dist/demo.css
vendored
1
demo/dist/demo.css
vendored
File diff suppressed because one or more lines are too long
26283
demo/dist/demo.js
vendored
26283
demo/dist/demo.js
vendored
File diff suppressed because it is too large
Load Diff
4
demo/dist/demo.min.js
vendored
4
demo/dist/demo.min.js
vendored
File diff suppressed because one or more lines are too long
1
demo/dist/demo.min.js.map
vendored
1
demo/dist/demo.min.js.map
vendored
File diff suppressed because one or more lines are too long
1
demo/dist/demo.svg
vendored
1
demo/dist/demo.svg
vendored
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 5.6 KiB |
1
demo/dist/error.css
vendored
1
demo/dist/error.css
vendored
File diff suppressed because one or more lines are too long
490
demo/index.html
490
demo/index.html
@@ -1,278 +1,260 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player</title>
|
||||
<meta
|
||||
name="description"
|
||||
property="og:description"
|
||||
content="A simple HTML5 media player with custom controls and WebVTT captions."
|
||||
/>
|
||||
<meta name="author" content="Sam Potts" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player</title>
|
||||
<meta
|
||||
name="description"
|
||||
property="og:description"
|
||||
content="A simple HTML5 media player with custom controls and WebVTT captions."
|
||||
/>
|
||||
<meta name="author" content="Sam Potts" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="icon" href="https://cdn.plyr.io/static/icons/favicon.ico" />
|
||||
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png" />
|
||||
<!-- Icons -->
|
||||
<link rel="icon" href="https://cdn.plyr.io/static/icons/favicon.ico" />
|
||||
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player"
|
||||
/>
|
||||
<meta property="og:site_name" content="Plyr" />
|
||||
<meta property="og:url" content="https://plyr.io" />
|
||||
<meta property="og:image" content="https://cdn.plyr.io/static/icons/1200x630.png" />
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player" />
|
||||
<meta property="og:site_name" content="Plyr" />
|
||||
<meta property="og:url" content="https://plyr.io" />
|
||||
<meta property="og:image" content="https://cdn.plyr.io/static/icons/1200x630.png" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@sam_potts" />
|
||||
<meta name="twitter:creator" content="@sam_potts" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@sam_potts" />
|
||||
<meta name="twitter:creator" content="@sam_potts" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
|
||||
<!-- Docs styles -->
|
||||
<link rel="stylesheet" href="dist/demo.css" />
|
||||
<!-- Docs styles -->
|
||||
<link rel="stylesheet" href="dist/demo.css" />
|
||||
|
||||
<!-- Preload -->
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
crossorigin
|
||||
type="font/woff2"
|
||||
href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
crossorigin
|
||||
type="font/woff2"
|
||||
href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2"
|
||||
/>
|
||||
<!-- Preload -->
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
crossorigin
|
||||
type="font/woff2"
|
||||
href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
crossorigin
|
||||
type="font/woff2"
|
||||
href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2"
|
||||
/>
|
||||
|
||||
<!-- Google Analytics-->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-132699580-1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'UA-132699580-1');
|
||||
</script>
|
||||
</head>
|
||||
<!-- Google Analytics-->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-132699580-1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'UA-132699580-1');
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="grid">
|
||||
<header>
|
||||
<h1>Pl<span>a</span>y<span>e</span>r</h1>
|
||||
<p>
|
||||
A simple, accessible and customisable media player for
|
||||
<button type="button" class="faux-link" data-source="video">
|
||||
<svg class="icon">
|
||||
<title>HTML5</title>
|
||||
<path
|
||||
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
|
||||
></path></svg
|
||||
>Video</button
|
||||
>,
|
||||
<button type="button" class="faux-link" data-source="audio">
|
||||
<svg class="icon">
|
||||
<title>HTML5</title>
|
||||
<path
|
||||
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
|
||||
></path></svg
|
||||
>Audio</button
|
||||
>,
|
||||
<button type="button" class="faux-link" data-source="youtube">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>YouTube</title>
|
||||
<path
|
||||
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
|
||||
<body>
|
||||
<div class="grid">
|
||||
<header>
|
||||
<h1>Pl<span>a</span>y<span>e</span>r</h1>
|
||||
<p>
|
||||
A simple, accessible and customisable media player for
|
||||
<button type="button" class="faux-link" data-source="video">
|
||||
<svg class="icon">
|
||||
<title>HTML5</title>
|
||||
<path
|
||||
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
|
||||
></path></svg
|
||||
>Video</button
|
||||
>,
|
||||
<button type="button" class="faux-link" data-source="audio">
|
||||
<svg class="icon">
|
||||
<title>HTML5</title>
|
||||
<path
|
||||
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
|
||||
></path></svg
|
||||
>Audio</button
|
||||
>,
|
||||
<button type="button" class="faux-link" data-source="youtube">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>YouTube</title>
|
||||
<path
|
||||
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
|
||||
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
|
||||
M6,11V5l5,3L6,11z"
|
||||
></path></svg
|
||||
>YouTube
|
||||
</button>
|
||||
and
|
||||
<button type="button" class="faux-link" data-source="vimeo">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>Vimeo</title>
|
||||
<path
|
||||
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
|
||||
></path></svg
|
||||
>YouTube
|
||||
</button>
|
||||
and
|
||||
<button type="button" class="faux-link" data-source="vimeo">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>Vimeo</title>
|
||||
<path
|
||||
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
|
||||
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
|
||||
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"
|
||||
></path></svg
|
||||
>Vimeo
|
||||
</button>
|
||||
</p>
|
||||
></path></svg
|
||||
>Vimeo
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Premium video monetization from
|
||||
<a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border">
|
||||
<img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi" />
|
||||
<span class="sr-only">ai.vi</span>
|
||||
</a>
|
||||
</p>
|
||||
<div class="call-to-action">
|
||||
<a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>GitHub</title>
|
||||
<path
|
||||
d="M8,0.2c-4.4,0-8,3.6-8,8c0,3.5,2.3,6.5,5.5,7.6
|
||||
C5.9,15.9,6,15.6,6,15.4c0-0.2,0-0.7,0-1.4C3.8,14.5,3.3,13,3.3,13c-0.4-0.9-0.9-1.2-0.9-1.2c-0.7-0.5,0.1-0.5,0.1-0.5
|
||||
c0.8,0.1,1.2,0.8,1.2,0.8C4.4,13.4,5.6,13,6,12.8c0.1-0.5,0.3-0.9,0.5-1.1c-1.8-0.2-3.6-0.9-3.6-4c0-0.9,0.3-1.6,0.8-2.1
|
||||
c-0.1-0.2-0.4-1,0.1-2.1c0,0,0.7-0.2,2.2,0.8c0.6-0.2,1.3-0.3,2-0.3c0.7,0,1.4,0.1,2,0.3c1.5-1,2.2-0.8,2.2-0.8
|
||||
c0.4,1.1,0.2,1.9,0.1,2.1c0.5,0.6,0.8,1.3,0.8,2.1c0,3.1-1.9,3.7-3.7,3.9C9.7,12,10,12.5,10,13.2c0,1.1,0,1.9,0,2.2
|
||||
c0,0.2,0.1,0.5,0.6,0.4c3.2-1.1,5.5-4.1,5.5-7.6C16,3.8,12.4,0.2,8,0.2z"
|
||||
></path>
|
||||
</svg>
|
||||
Download on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div id="container">
|
||||
<video
|
||||
controls
|
||||
crossorigin
|
||||
playsinline
|
||||
data-poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg"
|
||||
id="player"
|
||||
>
|
||||
<!-- Video files -->
|
||||
<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-1080p.mp4"
|
||||
type="video/mp4"
|
||||
size="1080"
|
||||
/>
|
||||
|
||||
<div class="call-to-action">
|
||||
<a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>GitHub</title>
|
||||
<path
|
||||
d="M8,0.2c-4.4,0-8,3.6-8,8c0,3.5,2.3,6.5,5.5,7.6
|
||||
C5.9,15.9,6,15.6,6,15.4c0-0.2,0-0.7,0-1.4C3.8,14.5,3.3,13,3.3,13c-0.4-0.9-0.9-1.2-0.9-1.2c-0.7-0.5,0.1-0.5,0.1-0.5
|
||||
c0.8,0.1,1.2,0.8,1.2,0.8C4.4,13.4,5.6,13,6,12.8c0.1-0.5,0.3-0.9,0.5-1.1c-1.8-0.2-3.6-0.9-3.6-4c0-0.9,0.3-1.6,0.8-2.1
|
||||
c-0.1-0.2-0.4-1,0.1-2.1c0,0,0.7-0.2,2.2,0.8c0.6-0.2,1.3-0.3,2-0.3c0.7,0,1.4,0.1,2,0.3c1.5-1,2.2-0.8,2.2-0.8
|
||||
c0.4,1.1,0.2,1.9,0.1,2.1c0.5,0.6,0.8,1.3,0.8,2.1c0,3.1-1.9,3.7-3.7,3.9C9.7,12,10,12.5,10,13.2c0,1.1,0,1.9,0,2.2
|
||||
c0,0.2,0.1,0.5,0.6,0.4c3.2-1.1,5.5-4.1,5.5-7.6C16,3.8,12.4,0.2,8,0.2z"
|
||||
></path>
|
||||
</svg>
|
||||
Download on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<!-- 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"
|
||||
default
|
||||
/>
|
||||
<track
|
||||
kind="captions"
|
||||
label="Français"
|
||||
srclang="fr"
|
||||
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"
|
||||
/>
|
||||
|
||||
<main>
|
||||
<div id="container">
|
||||
<video
|
||||
controls
|
||||
crossorigin
|
||||
playsinline
|
||||
poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg"
|
||||
id="player"
|
||||
>
|
||||
<!-- Video files -->
|
||||
<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-1080p.mp4"
|
||||
type="video/mp4"
|
||||
size="1080"
|
||||
/>
|
||||
|
||||
<!-- 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"
|
||||
default
|
||||
/>
|
||||
<track
|
||||
kind="captions"
|
||||
label="Français"
|
||||
srclang="fr"
|
||||
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"
|
||||
/>
|
||||
|
||||
<!-- Fallback for browsers that don't support the <video> element -->
|
||||
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download
|
||||
>Download</a
|
||||
>
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<li class="plyr__cite plyr__cite--video" hidden>
|
||||
<small>
|
||||
<svg class="icon">
|
||||
<title>HTML5</title>
|
||||
<path
|
||||
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
|
||||
></path>
|
||||
</svg>
|
||||
<a
|
||||
href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323"
|
||||
target="_blank"
|
||||
>View From A Blue Moon</a
|
||||
>
|
||||
© Brainfarm
|
||||
</small>
|
||||
</li>
|
||||
<li class="plyr__cite plyr__cite--audio" hidden>
|
||||
<small>
|
||||
<svg class="icon" title="HTML5">
|
||||
<title>HTML5</title>
|
||||
<path
|
||||
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
|
||||
></path>
|
||||
</svg>
|
||||
<a href="http://www.kishibashi.com/" target="_blank"
|
||||
>Kishi Bashi – “It All Began With A Burst”</a
|
||||
>
|
||||
© Kishi Bashi
|
||||
</small>
|
||||
</li>
|
||||
<li class="plyr__cite plyr__cite--youtube" hidden>
|
||||
<small>
|
||||
<a href="https://www.youtube.com/watch?v=bTqVqk7FSmY" target="_blank"
|
||||
>View From A Blue Moon</a
|
||||
>
|
||||
on
|
||||
<span class="color--youtube">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>YouTube</title>
|
||||
<path
|
||||
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
|
||||
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
|
||||
M6,11V5l5,3L6,11z"
|
||||
></path></svg
|
||||
>YouTube
|
||||
</span>
|
||||
</small>
|
||||
</li>
|
||||
<li class="plyr__cite plyr__cite--vimeo" hidden>
|
||||
<small>
|
||||
<a href="https://vimeo.com/40648169" target="_blank">Toob “Wavaphon” Music Video</a>
|
||||
on
|
||||
<span class="color--vimeo">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>Vimeo</title>
|
||||
<path
|
||||
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
|
||||
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
|
||||
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"
|
||||
></path></svg
|
||||
>Vimeo
|
||||
</span>
|
||||
</small>
|
||||
</li>
|
||||
</ul>
|
||||
</main>
|
||||
<!-- Fallback for browsers that don't support the <video> element -->
|
||||
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<aside>
|
||||
<svg class="icon">
|
||||
<title>Twitter</title>
|
||||
<ul>
|
||||
<li class="plyr__cite plyr__cite--video" hidden>
|
||||
<small>
|
||||
<svg class="icon">
|
||||
<title>HTML5</title>
|
||||
<path
|
||||
d="M16,3c-0.6,0.3-1.2,0.4-1.9,0.5c0.7-0.4,1.2-1,1.4-1.8c-0.6,0.4-1.3,0.6-2.1,0.8c-0.6-0.6-1.5-1-2.4-1
|
||||
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
|
||||
></path>
|
||||
</svg>
|
||||
<a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank"
|
||||
>View From A Blue Moon</a
|
||||
>
|
||||
© Brainfarm
|
||||
</small>
|
||||
</li>
|
||||
<li class="plyr__cite plyr__cite--audio" hidden>
|
||||
<small>
|
||||
<svg class="icon" title="HTML5">
|
||||
<title>HTML5</title>
|
||||
<path
|
||||
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
|
||||
></path>
|
||||
</svg>
|
||||
<a href="http://www.kishibashi.com/" target="_blank"
|
||||
>Kishi Bashi – “It All Began With A Burst”</a
|
||||
>
|
||||
© Kishi Bashi
|
||||
</small>
|
||||
</li>
|
||||
<li class="plyr__cite plyr__cite--youtube" hidden>
|
||||
<small>
|
||||
<a href="https://www.youtube.com/watch?v=bTqVqk7FSmY" target="_blank">View From A Blue Moon</a>
|
||||
on
|
||||
<span class="color--youtube">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>YouTube</title>
|
||||
<path
|
||||
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
|
||||
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
|
||||
M6,11V5l5,3L6,11z"
|
||||
></path></svg
|
||||
>YouTube
|
||||
</span>
|
||||
</small>
|
||||
</li>
|
||||
<li class="plyr__cite plyr__cite--vimeo" hidden>
|
||||
<small>
|
||||
<a href="https://vimeo.com/40648169" target="_blank">Toob “Wavaphon” Music Video</a>
|
||||
on
|
||||
<span class="color--vimeo">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>Vimeo</title>
|
||||
<path
|
||||
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
|
||||
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
|
||||
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"
|
||||
></path></svg
|
||||
>Vimeo
|
||||
</span>
|
||||
</small>
|
||||
</li>
|
||||
</ul>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<aside>
|
||||
<svg class="icon">
|
||||
<title>Twitter</title>
|
||||
<path
|
||||
d="M16,3c-0.6,0.3-1.2,0.4-1.9,0.5c0.7-0.4,1.2-1,1.4-1.8c-0.6,0.4-1.3,0.6-2.1,0.8c-0.6-0.6-1.5-1-2.4-1
|
||||
C9.3,1.5,7.8,3,7.8,4.8c0,0.3,0,0.5,0.1,0.7C5.2,5.4,2.7,4.1,1.1,2.1c-0.3,0.5-0.4,1-0.4,1.7c0,1.1,0.6,2.1,1.5,2.7
|
||||
c-0.5,0-1-0.2-1.5-0.4c0,0,0,0,0,0c0,1.6,1.1,2.9,2.6,3.2C3,9.4,2.7,9.4,2.4,9.4c-0.2,0-0.4,0-0.6-0.1c0.4,1.3,1.6,2.3,3.1,2.3
|
||||
c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"
|
||||
></path>
|
||||
</svg>
|
||||
<p>
|
||||
If you think Plyr's good,
|
||||
<a
|
||||
href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&url=http%3A%2F%2Fplyr.io&via=Sam_Potts"
|
||||
target="_blank"
|
||||
class="js-shr"
|
||||
>tweet it</a
|
||||
>
|
||||
👍
|
||||
</p>
|
||||
</aside>
|
||||
></path>
|
||||
</svg>
|
||||
<p>
|
||||
If you think Plyr's good,
|
||||
<a
|
||||
href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&url=http%3A%2F%2Fplyr.io&via=Sam_Potts"
|
||||
target="_blank"
|
||||
class="js-shr"
|
||||
>tweet it</a
|
||||
>
|
||||
👍
|
||||
</p>
|
||||
</aside>
|
||||
|
||||
<script src="dist/demo.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
<script src="dist/demo.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "plyr-demo",
|
||||
"version": "1.0.0",
|
||||
"description": "Demo for Plyr",
|
||||
"homepage": "https://plyr.io",
|
||||
"author": "Sam Potts <sam@potts.es>",
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.4",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"raven-js": "^3.27.2",
|
||||
"shr-buttons": "2.0.3",
|
||||
"url-polyfill": "^1.1.8"
|
||||
}
|
||||
"name": "plyr-demo",
|
||||
"version": "1.0.0",
|
||||
"description": "Demo for Plyr",
|
||||
"homepage": "https://plyr.io",
|
||||
"author": "Sam Potts <sam@potts.es>",
|
||||
"dependencies": {
|
||||
"@sentry/browser": "^5.22.3",
|
||||
"core-js": "^3.6.5",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"shr-buttons": "2.0.3",
|
||||
"url-polyfill": "^1.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// ==========================================================================
|
||||
// Plyr.io demo
|
||||
// This code is purely for the https://plyr.io website
|
||||
// Please see readme.md in the root or github.com/sampotts/plyr
|
||||
// Please see README.md in the root or github.com/sampotts/plyr
|
||||
// ==========================================================================
|
||||
|
||||
import './tab-focus';
|
||||
import 'custom-event-polyfill';
|
||||
import 'url-polyfill';
|
||||
|
||||
import Raven from 'raven-js';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import Shr from 'shr-buttons';
|
||||
|
||||
import Plyr from '../../../src/js/plyr';
|
||||
@@ -16,145 +16,162 @@ import sources from './sources';
|
||||
import toggleClass from './toggle-class';
|
||||
|
||||
(() => {
|
||||
const { host } = window.location;
|
||||
const env = {
|
||||
prod: host === 'plyr.io',
|
||||
dev: host === 'dev.plyr.io',
|
||||
};
|
||||
const production = 'plyr.io';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
Raven.context(() => {
|
||||
const selector = '#player';
|
||||
// Sentry for demo site (https://plyr.io) only
|
||||
if (window.location.host === production) {
|
||||
Sentry.init({
|
||||
dsn: 'https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555',
|
||||
whitelistUrls: [production].map((d) => new RegExp(`https://(([a-z0-9])+(.))*${d}`)),
|
||||
});
|
||||
}
|
||||
|
||||
// Setup share buttons
|
||||
Shr.setup('.js-shr', {
|
||||
count: {
|
||||
className: 'button__count',
|
||||
},
|
||||
wrapper: {
|
||||
className: 'button--with-count',
|
||||
},
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const selector = '#player';
|
||||
|
||||
// Setup the player
|
||||
const player = new Plyr(selector, {
|
||||
debug: true,
|
||||
title: 'View From A Blue Moon',
|
||||
iconUrl: 'dist/demo.svg',
|
||||
keyboard: {
|
||||
global: true,
|
||||
},
|
||||
tooltips: {
|
||||
controls: true,
|
||||
},
|
||||
captions: {
|
||||
active: true,
|
||||
},
|
||||
ads: {
|
||||
enabled: env.prod || env.dev,
|
||||
publisherId: '918848828995742',
|
||||
},
|
||||
previewThumbnails: {
|
||||
enabled: true,
|
||||
src: [
|
||||
'https://cdn.plyr.io/static/demo/thumbs/100p.vtt',
|
||||
'https://cdn.plyr.io/static/demo/thumbs/240p.vtt',
|
||||
],
|
||||
},
|
||||
vimeo: {
|
||||
// Prevent Vimeo blocking plyr.io demo site
|
||||
referrerPolicy: 'no-referrer',
|
||||
},
|
||||
});
|
||||
|
||||
// Expose for tinkering in the console
|
||||
window.player = player;
|
||||
|
||||
// Setup type toggle
|
||||
const buttons = document.querySelectorAll('[data-source]');
|
||||
const types = Object.keys(sources);
|
||||
const historySupport = Boolean(window.history && window.history.pushState);
|
||||
let currentType = window.location.hash.substring(1);
|
||||
const hasCurrentType = !currentType.length;
|
||||
|
||||
function render(type) {
|
||||
// Remove active classes
|
||||
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
|
||||
|
||||
// Set active on parent
|
||||
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
|
||||
|
||||
// Show cite
|
||||
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
cite.hidden = true;
|
||||
});
|
||||
|
||||
document.querySelector(`.plyr__cite--${type}`).hidden = false;
|
||||
}
|
||||
|
||||
// Set a new source
|
||||
function setSource(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
|
||||
if (
|
||||
!types.includes(type) ||
|
||||
(!init && type === currentType) ||
|
||||
(!currentType.length && type === 'video')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the new source
|
||||
player.source = sources[type];
|
||||
|
||||
// Set the current type for next time
|
||||
currentType = type;
|
||||
|
||||
render(type);
|
||||
}
|
||||
|
||||
// Bind to each button
|
||||
Array.from(buttons).forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const type = button.getAttribute('data-source');
|
||||
|
||||
setSource(type);
|
||||
|
||||
if (historySupport) {
|
||||
window.history.pushState({ type }, '', `#${type}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// List for backwards/forwards
|
||||
window.addEventListener('popstate', event => {
|
||||
if (event.state && Object.keys(event.state).includes('type')) {
|
||||
setSource(event.state.type);
|
||||
}
|
||||
});
|
||||
|
||||
// If there's no current type set, assume video
|
||||
if (hasCurrentType) {
|
||||
currentType = 'video';
|
||||
}
|
||||
|
||||
// Replace current history state
|
||||
if (historySupport && types.includes(currentType)) {
|
||||
window.history.replaceState({ type: currentType }, '', hasCurrentType ? '' : `#${currentType}`);
|
||||
}
|
||||
|
||||
// If it's not video, load the source
|
||||
if (currentType !== 'video') {
|
||||
setSource(currentType, true);
|
||||
}
|
||||
|
||||
render(currentType);
|
||||
});
|
||||
// Setup share buttons
|
||||
Shr.setup('.js-shr', {
|
||||
count: {
|
||||
className: 'button__count',
|
||||
},
|
||||
wrapper: {
|
||||
className: 'button--with-count',
|
||||
},
|
||||
});
|
||||
|
||||
// Raven / Sentry
|
||||
// For demo site (https://plyr.io) only
|
||||
if (env.prod) {
|
||||
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
|
||||
// Setup the player
|
||||
const player = new Plyr(selector, {
|
||||
debug: true,
|
||||
title: 'View From A Blue Moon',
|
||||
iconUrl: 'dist/demo.svg',
|
||||
keyboard: {
|
||||
global: true,
|
||||
},
|
||||
tooltips: {
|
||||
controls: true,
|
||||
},
|
||||
captions: {
|
||||
active: true,
|
||||
},
|
||||
ads: {
|
||||
enabled: window.location.host.includes(production),
|
||||
publisherId: '918848828995742',
|
||||
},
|
||||
previewThumbnails: {
|
||||
enabled: true,
|
||||
src: ['https://cdn.plyr.io/static/demo/thumbs/100p.vtt', 'https://cdn.plyr.io/static/demo/thumbs/240p.vtt'],
|
||||
},
|
||||
vimeo: {
|
||||
// Prevent Vimeo blocking plyr.io demo site
|
||||
referrerPolicy: 'no-referrer',
|
||||
},
|
||||
mediaMetadata: {
|
||||
title: 'View From A Blue Moon',
|
||||
album: 'Sports',
|
||||
artist: 'Brainfarm',
|
||||
artwork: [
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
},
|
||||
markers: {
|
||||
enabled: true,
|
||||
points: [
|
||||
{
|
||||
time: 10,
|
||||
label: 'first marker',
|
||||
},
|
||||
{
|
||||
time: 40,
|
||||
label: 'second marker',
|
||||
},
|
||||
{
|
||||
time: 120,
|
||||
label: '<strong>third</strong> marker',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Expose for tinkering in the console
|
||||
window.player = player;
|
||||
|
||||
// Setup type toggle
|
||||
const buttons = document.querySelectorAll('[data-source]');
|
||||
const types = Object.keys(sources);
|
||||
const historySupport = Boolean(window.history && window.history.pushState);
|
||||
let currentType = window.location.hash.substring(1);
|
||||
const hasInitialType = currentType.length;
|
||||
|
||||
function render(type) {
|
||||
// Remove active classes
|
||||
Array.from(buttons).forEach((button) => toggleClass(button.parentElement, 'active', false));
|
||||
|
||||
// Set active on parent
|
||||
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
|
||||
|
||||
// Show cite
|
||||
Array.from(document.querySelectorAll('.plyr__cite')).forEach((cite) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
cite.hidden = true;
|
||||
});
|
||||
|
||||
document.querySelector(`.plyr__cite--${type}`).hidden = false;
|
||||
}
|
||||
|
||||
// Set a new source
|
||||
function setSource(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
|
||||
if (!types.includes(type) || (!init && type === currentType) || (!currentType.length && type === 'video')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the new source
|
||||
player.source = sources[type];
|
||||
|
||||
// Set the current type for next time
|
||||
currentType = type;
|
||||
|
||||
render(type);
|
||||
}
|
||||
|
||||
// Bind to each button
|
||||
Array.from(buttons).forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const type = button.getAttribute('data-source');
|
||||
|
||||
setSource(type);
|
||||
|
||||
if (historySupport) {
|
||||
window.history.pushState({ type }, '', `#${type}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// List for backwards/forwards
|
||||
window.addEventListener('popstate', (event) => {
|
||||
if (event.state && Object.keys(event.state).includes('type')) {
|
||||
setSource(event.state.type);
|
||||
}
|
||||
});
|
||||
|
||||
// If there's no current type set, assume video
|
||||
if (!hasInitialType) {
|
||||
currentType = 'video';
|
||||
}
|
||||
|
||||
// Replace current history state
|
||||
if (historySupport && types.includes(currentType)) {
|
||||
window.history.replaceState({ type: currentType }, '', hasInitialType ? `#${currentType}` : '');
|
||||
}
|
||||
|
||||
// If it's not video, load the source
|
||||
if (currentType !== 'video') {
|
||||
setSource(currentType, true);
|
||||
}
|
||||
|
||||
render(currentType);
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -1,78 +1,81 @@
|
||||
const sources = {
|
||||
video: {
|
||||
type: 'video',
|
||||
title: 'View From A Blue Moon',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 576,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 720,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 1080,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 1440,
|
||||
},
|
||||
],
|
||||
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
|
||||
tracks: [
|
||||
{
|
||||
kind: 'captions',
|
||||
label: 'English',
|
||||
srclang: 'en',
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
kind: 'captions',
|
||||
label: 'French',
|
||||
srclang: 'fr',
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
|
||||
},
|
||||
],
|
||||
},
|
||||
audio: {
|
||||
type: 'audio',
|
||||
title: 'Kishi Bashi – “It All Began With A Burst”',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
|
||||
type: 'audio/mp3',
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
|
||||
type: 'audio/ogg',
|
||||
},
|
||||
],
|
||||
},
|
||||
youtube: {
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
|
||||
provider: 'youtube',
|
||||
},
|
||||
],
|
||||
},
|
||||
vimeo: {
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://vimeo.com/40648169',
|
||||
provider: 'vimeo',
|
||||
},
|
||||
],
|
||||
video: {
|
||||
type: 'video',
|
||||
title: 'View From A Blue Moon',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 576,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 720,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 1080,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 1440,
|
||||
},
|
||||
],
|
||||
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
|
||||
tracks: [
|
||||
{
|
||||
kind: 'captions',
|
||||
label: 'English',
|
||||
srclang: 'en',
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
kind: 'captions',
|
||||
label: 'French',
|
||||
srclang: 'fr',
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
|
||||
},
|
||||
],
|
||||
previewThumbnails: {
|
||||
src: ['https://cdn.plyr.io/static/demo/thumbs/100p.vtt', 'https://cdn.plyr.io/static/demo/thumbs/240p.vtt'],
|
||||
},
|
||||
},
|
||||
audio: {
|
||||
type: 'audio',
|
||||
title: 'Kishi Bashi – “It All Began With A Burst”',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
|
||||
type: 'audio/mp3',
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
|
||||
type: 'audio/ogg',
|
||||
},
|
||||
],
|
||||
},
|
||||
youtube: {
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
|
||||
provider: 'youtube',
|
||||
},
|
||||
],
|
||||
},
|
||||
vimeo: {
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://vimeo.com/40648169',
|
||||
provider: 'vimeo',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default sources;
|
||||
|
||||
@@ -3,29 +3,29 @@ const container = document.getElementById('container');
|
||||
const tabClassName = 'tab-focus';
|
||||
|
||||
// Remove class on blur
|
||||
document.addEventListener('focusout', event => {
|
||||
if (!event.target.classList || container.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
document.addEventListener('focusout', (event) => {
|
||||
if (!event.target.classList || container.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.target.classList.remove(tabClassName);
|
||||
event.target.classList.remove(tabClassName);
|
||||
});
|
||||
|
||||
// Add classname to tabbed elements
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.keyCode !== 9) {
|
||||
return;
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.keyCode !== 9) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delay the adding of classname until the focus has changed
|
||||
// This event fires before the focusin event
|
||||
setTimeout(() => {
|
||||
const focused = document.activeElement;
|
||||
|
||||
if (!focused || !focused.classList || container.contains(focused)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delay the adding of classname until the focus has changed
|
||||
// This event fires before the focusin event
|
||||
setTimeout(() => {
|
||||
const focused = document.activeElement;
|
||||
|
||||
if (!focused || !focused.classList || container.contains(focused)) {
|
||||
return;
|
||||
}
|
||||
|
||||
focused.classList.add(tabClassName);
|
||||
}, 10);
|
||||
focused.classList.add(tabClassName);
|
||||
}, 10);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Toggle class on an element
|
||||
const toggleClass = (element, className = '', toggle = false) =>
|
||||
element && element.classList[toggle ? 'add' : 'remove'](className);
|
||||
element && element.classList[toggle ? 'add' : 'remove'](className);
|
||||
|
||||
export default toggleClass;
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// Plyr.io Demo Page
|
||||
// ==========================================================================
|
||||
@charset 'UTF-8';
|
||||
@import '../../../../src/sass/lib/css-vars';
|
||||
|
||||
$css-vars-use-native: true;
|
||||
|
||||
// Settings
|
||||
@import '../settings/breakpoints';
|
||||
|
||||
@@ -5,80 +5,80 @@
|
||||
// Shared
|
||||
.button,
|
||||
.button__count {
|
||||
align-items: center;
|
||||
border: 0;
|
||||
border-radius: $border-radius-base;
|
||||
box-shadow: 0 1px 1px rgba(#000, 0.1);
|
||||
display: inline-flex;
|
||||
padding: ($spacing-base * 0.75);
|
||||
position: relative;
|
||||
text-shadow: none;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
border-radius: $border-radius-base;
|
||||
box-shadow: 0 1px 1px rgba(#000, 0.1);
|
||||
display: inline-flex;
|
||||
padding: ($spacing-base * 0.75);
|
||||
position: relative;
|
||||
text-shadow: none;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// Buttons
|
||||
.button {
|
||||
background: $color-button-background;
|
||||
color: $color-button-text;
|
||||
font-weight: $font-weight-bold;
|
||||
padding-left: ($spacing-base * 1.25);
|
||||
padding-right: ($spacing-base * 1.25);
|
||||
transition: all 0.2s ease;
|
||||
background: $color-button-background;
|
||||
color: $color-button-text;
|
||||
font-weight: $font-weight-bold;
|
||||
padding-left: ($spacing-base * 1.25);
|
||||
padding-right: ($spacing-base * 1.25);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: $color-button-background-hover;
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: $color-button-background-hover;
|
||||
|
||||
// Remove the underline/border
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
// Remove the underline/border
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 2px rgba(#000, 0.1);
|
||||
}
|
||||
&:hover {
|
||||
box-shadow: 0 2px 2px rgba(#000, 0.1);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&.tab-focus {
|
||||
@include tab-focus();
|
||||
}
|
||||
&.tab-focus {
|
||||
@include tab-focus;
|
||||
}
|
||||
|
||||
&:active {
|
||||
top: 1px;
|
||||
}
|
||||
&:active {
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
// Button group
|
||||
.button--with-count {
|
||||
display: inline-flex;
|
||||
display: inline-flex;
|
||||
|
||||
.button .icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.button .icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Count bubble
|
||||
.button__count {
|
||||
animation: fadein 0.2s ease;
|
||||
background: $color-button-count-background;
|
||||
color: $color-button-count-text;
|
||||
margin-left: ($spacing-base * 0.75);
|
||||
animation: fade-in 0.2s ease;
|
||||
background: $color-button-count-background;
|
||||
color: $color-button-count-text;
|
||||
margin-left: ($spacing-base * 0.75);
|
||||
|
||||
&::before {
|
||||
border: $arrow-size solid transparent;
|
||||
border-left-width: 0;
|
||||
border-right-color: $color-button-count-background;
|
||||
content: '';
|
||||
height: 0;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0;
|
||||
}
|
||||
&::before {
|
||||
border: $arrow-size solid transparent;
|
||||
border-left-width: 0;
|
||||
border-right-color: $color-button-count-background;
|
||||
content: '';
|
||||
height: 0;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,28 +3,28 @@
|
||||
// ==========================================================================
|
||||
|
||||
header {
|
||||
padding-bottom: $spacing-base;
|
||||
text-align: center;
|
||||
padding-bottom: $spacing-base;
|
||||
text-align: center;
|
||||
|
||||
h1 span {
|
||||
animation: shrinkHide 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) 2s forwards;
|
||||
display: inline-block;
|
||||
font-weight: $font-weight-light;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.call-to-action {
|
||||
margin-top: ($spacing-base * 1.5);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $screen-md) {
|
||||
margin-right: ($spacing-base * 3);
|
||||
max-width: 360px;
|
||||
padding-bottom: ($spacing-base * 2);
|
||||
text-align: left;
|
||||
|
||||
p:first-of-type {
|
||||
@include font-size($font-size-base + 1);
|
||||
}
|
||||
h1 span {
|
||||
animation: shrink-hide 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) 2s forwards;
|
||||
display: inline-block;
|
||||
font-weight: $font-weight-light;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.call-to-action {
|
||||
margin-top: ($spacing-base * 1.5);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $screen-md) {
|
||||
margin-right: ($spacing-base * 3);
|
||||
max-width: 360px;
|
||||
padding-bottom: ($spacing-base * 2);
|
||||
text-align: left;
|
||||
|
||||
p:first-of-type {
|
||||
@include font-size($font-size-base + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,24 @@
|
||||
// Icons
|
||||
// ==========================================================================
|
||||
|
||||
@use 'sass:math';
|
||||
|
||||
// Base size icon styles
|
||||
.icon {
|
||||
fill: currentColor;
|
||||
height: $icon-size;
|
||||
vertical-align: -3px;
|
||||
width: $icon-size;
|
||||
fill: currentColor;
|
||||
height: $icon-size;
|
||||
vertical-align: -3px;
|
||||
width: $icon-size;
|
||||
}
|
||||
|
||||
// Within elements
|
||||
a svg,
|
||||
button svg,
|
||||
label svg {
|
||||
pointer-events: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
a .icon,
|
||||
.btn .icon {
|
||||
margin-right: ($spacing-base / 2);
|
||||
margin-right: math.div($spacing-base, 4);
|
||||
}
|
||||
|
||||
@@ -4,45 +4,45 @@
|
||||
|
||||
// Make a <button> look like an <a>
|
||||
button.faux-link {
|
||||
@extend a; // stylelint-disable-line
|
||||
@include cancel-button-styles();
|
||||
@extend a; // stylelint-disable-line
|
||||
@include cancel-button-styles;
|
||||
}
|
||||
|
||||
// Links
|
||||
a {
|
||||
border-bottom: 1px dotted currentColor;
|
||||
color: $color-link;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 1px dotted currentColor;
|
||||
color: $color-link;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&::after {
|
||||
background: currentColor;
|
||||
content: '';
|
||||
height: 1px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
transform: translateX(-50%);
|
||||
transition: width 0.2s ease;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-bottom-color: transparent;
|
||||
outline: 0;
|
||||
|
||||
&::after {
|
||||
background: currentColor;
|
||||
content: '';
|
||||
height: 1px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
transform: translateX(-50%);
|
||||
transition: width 0.2s ease;
|
||||
width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-bottom-color: transparent;
|
||||
outline: 0;
|
||||
&.tab-focus {
|
||||
@include tab-focus;
|
||||
}
|
||||
|
||||
&::after {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.tab-focus {
|
||||
@include tab-focus();
|
||||
}
|
||||
|
||||
&.no-border::after {
|
||||
display: none;
|
||||
}
|
||||
&.no-border::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Lists
|
||||
ul,
|
||||
li {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
img,
|
||||
video,
|
||||
audio {
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// ==========================================================================
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: $spacing-base;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: $spacing-base;
|
||||
}
|
||||
|
||||
@@ -2,35 +2,37 @@
|
||||
// Examples
|
||||
// ==========================================================================
|
||||
|
||||
@use 'sass:math';
|
||||
|
||||
// Example players
|
||||
.plyr {
|
||||
border-radius: $border-radius-base;
|
||||
box-shadow: 0 2px 15px rgba(#000, 0.1);
|
||||
margin: $spacing-base auto;
|
||||
border-radius: $border-radius-large;
|
||||
box-shadow: 0 2px 15px rgba(#000, 0.1);
|
||||
margin: $spacing-base auto;
|
||||
|
||||
&.plyr--audio {
|
||||
max-width: 480px;
|
||||
}
|
||||
&.plyr--audio {
|
||||
max-width: 480px;
|
||||
}
|
||||
}
|
||||
|
||||
.plyr__video-wrapper::after {
|
||||
border: 1px solid rgba(#000, 0.15);
|
||||
border-radius: inherit;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
border: 1px solid rgba(#000, 0.15);
|
||||
border-radius: inherit;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
// Style full supported player
|
||||
.plyr__cite {
|
||||
color: $color-gray-5;
|
||||
color: $color-gray-500;
|
||||
|
||||
.icon {
|
||||
margin-right: ceil($spacing-base / 6);
|
||||
}
|
||||
.icon {
|
||||
margin-right: math.div($spacing-base, 6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,62 +2,64 @@
|
||||
// Core
|
||||
// ==========================================================================
|
||||
|
||||
@use 'sass:math';
|
||||
|
||||
html,
|
||||
body {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
background: $page-background;
|
||||
background-attachment: fixed;
|
||||
height: 100%;
|
||||
background: $page-background;
|
||||
background-attachment: fixed;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.grid {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
main {
|
||||
margin: auto;
|
||||
padding-bottom: 1px; // Collapsing margins
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
padding-bottom: 1px; // Collapsing margins
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
aside {
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
padding: $spacing-base;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
text-shadow: none;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
padding: $spacing-base;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
text-shadow: none;
|
||||
width: 100%;
|
||||
|
||||
.icon {
|
||||
fill: $color-twitter;
|
||||
margin-right: ($spacing-base / 2);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color-twitter;
|
||||
|
||||
&.tab-focus {
|
||||
@include tab-focus($color-twitter);
|
||||
}
|
||||
.icon {
|
||||
fill: $color-twitter;
|
||||
margin-right: math.div($spacing-base, 2);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color-twitter;
|
||||
|
||||
&.tab-focus {
|
||||
@include tab-focus($color-twitter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,26 +5,26 @@
|
||||
// Error page
|
||||
html.error,
|
||||
.error body {
|
||||
height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html.error {
|
||||
background: $page-background;
|
||||
background-attachment: fixed;
|
||||
background: $page-background;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
.error body {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error main {
|
||||
padding: $spacing-base;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: $spacing-base;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
p {
|
||||
@include font-size($font-size-large);
|
||||
}
|
||||
p {
|
||||
@include font-size($font-size-large);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
// ==========================================================================
|
||||
|
||||
.grid {
|
||||
margin: 0 auto;
|
||||
padding: $spacing-base;
|
||||
margin: 0 auto;
|
||||
padding: $spacing-base;
|
||||
|
||||
@media only screen and (min-width: $screen-md) {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
max-width: $container-max-width;
|
||||
width: 100%;
|
||||
@media only screen and (min-width: $screen-md) {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
max-width: $container-max-width;
|
||||
width: 100%;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,25 +3,28 @@
|
||||
// ==========================================================================
|
||||
|
||||
// Fade
|
||||
@keyframes fadein {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shrinkHide {
|
||||
0% {
|
||||
opacity: 0.5;
|
||||
width: 38px;
|
||||
}
|
||||
20% {
|
||||
width: 45px;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
}
|
||||
@keyframes shrink-hide {
|
||||
0% {
|
||||
opacity: 0.5;
|
||||
width: 38px;
|
||||
}
|
||||
|
||||
20% {
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,46 +3,46 @@
|
||||
// ==========================================================================
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Gordita';
|
||||
font-style: normal;
|
||||
font-weight: $font-weight-light;
|
||||
src: url('https://cdn.plyr.io/static/fonts/gordita-light.woff2') format('woff2'),
|
||||
url('https://cdn.plyr.io/static/fonts/gordita-light.woff') format('woff');
|
||||
font-display: swap;
|
||||
font-family: Gordita;
|
||||
font-style: normal;
|
||||
font-weight: $font-weight-light;
|
||||
src: url('https://cdn.plyr.io/static/fonts/gordita-light.woff2') format('woff2'),
|
||||
url('https://cdn.plyr.io/static/fonts/gordita-light.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Gordita';
|
||||
font-style: normal;
|
||||
font-weight: $font-weight-regular;
|
||||
src: url('https://cdn.plyr.io/static/fonts/gordita-regular.woff2') format('woff2'),
|
||||
url('https://cdn.plyr.io/static/fonts/gordita-regular.woff') format('woff');
|
||||
font-display: swap;
|
||||
font-family: Gordita;
|
||||
font-style: normal;
|
||||
font-weight: $font-weight-regular;
|
||||
src: url('https://cdn.plyr.io/static/fonts/gordita-regular.woff2') format('woff2'),
|
||||
url('https://cdn.plyr.io/static/fonts/gordita-regular.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Gordita';
|
||||
font-style: normal;
|
||||
font-weight: $font-weight-medium;
|
||||
src: url('https://cdn.plyr.io/static/fonts/gordita-medium.woff2') format('woff2'),
|
||||
url('https://cdn.plyr.io/static/fonts/gordita-medium.woff') format('woff');
|
||||
font-display: swap;
|
||||
font-family: Gordita;
|
||||
font-style: normal;
|
||||
font-weight: $font-weight-medium;
|
||||
src: url('https://cdn.plyr.io/static/fonts/gordita-medium.woff2') format('woff2'),
|
||||
url('https://cdn.plyr.io/static/fonts/gordita-medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Gordita';
|
||||
font-style: normal;
|
||||
font-weight: $font-weight-bold;
|
||||
src: url('https://cdn.plyr.io/static/fonts/gordita-bold.woff2') format('woff2'),
|
||||
url('https://cdn.plyr.io/static/fonts/gordita-bold.woff') format('woff');
|
||||
font-display: swap;
|
||||
font-family: Gordita;
|
||||
font-style: normal;
|
||||
font-weight: $font-weight-bold;
|
||||
src: url('https://cdn.plyr.io/static/fonts/gordita-bold.woff2') format('woff2'),
|
||||
url('https://cdn.plyr.io/static/fonts/gordita-bold.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Gordita';
|
||||
font-style: normal;
|
||||
font-weight: $font-weight-black;
|
||||
src: url('https://cdn.plyr.io/static/fonts/gordita-black.woff2') format('woff2'),
|
||||
url('https://cdn.plyr.io/static/fonts/gordita-black.woff') format('woff');
|
||||
font-display: swap;
|
||||
font-family: Gordita;
|
||||
font-style: normal;
|
||||
font-weight: $font-weight-black;
|
||||
src: url('https://cdn.plyr.io/static/fonts/gordita-black.woff2') format('woff2'),
|
||||
url('https://cdn.plyr.io/static/fonts/gordita-black.woff') format('woff');
|
||||
}
|
||||
|
||||
@@ -2,53 +2,55 @@
|
||||
// Mixins
|
||||
// ==========================================================================
|
||||
|
||||
@use 'sass:math';
|
||||
|
||||
// Convert a <button> into an <a>
|
||||
// ---------------------------------------
|
||||
@mixin cancel-button-styles() {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
line-height: $line-height-base;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
text-align: inherit;
|
||||
text-shadow: inherit;
|
||||
-moz-user-select: text; // stylelint-disable-line
|
||||
vertical-align: baseline;
|
||||
width: auto;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
line-height: $line-height-base;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
text-align: inherit;
|
||||
text-shadow: inherit;
|
||||
user-select: text;
|
||||
vertical-align: baseline;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// Nicer focus styles
|
||||
// ---------------------------------------
|
||||
@mixin tab-focus($color: $tab-focus-default-color) {
|
||||
box-shadow: 0 0 0 3px rgba($color, 0.35);
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 3px rgba($color, 0.35);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
// Use rems for font sizing
|
||||
// Leave <body> at 100%/16px
|
||||
// ---------------------------------------
|
||||
@function calculate-rem($size) {
|
||||
$rem: $size / 16;
|
||||
@return #{$rem}rem;
|
||||
$rem: math.div($size, 16);
|
||||
@return #{$rem}rem;
|
||||
}
|
||||
|
||||
@mixin font-size($size: $font-size-base) {
|
||||
font-size: $size * 1px; // Fallback in px
|
||||
font-size: calculate-rem($size);
|
||||
font-size: $size * 1px; // Fallback in px
|
||||
font-size: calculate-rem($size);
|
||||
}
|
||||
|
||||
// Font smoothing
|
||||
// ---------------------------------------
|
||||
@mixin font-smoothing($enabled: true) {
|
||||
@if $enabled {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
} @else {
|
||||
-moz-osx-font-smoothing: auto;
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
}
|
||||
@if $enabled {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
} @else {
|
||||
-moz-osx-font-smoothing: auto;
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
147
demo/src/sass/lib/normalize.scss
vendored
147
demo/src/sass/lib/normalize.scss
vendored
@@ -10,9 +10,8 @@
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
line-height: 1.15; /* 1 */
|
||||
text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
@@ -23,7 +22,7 @@ html {
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,7 +35,7 @@ footer,
|
||||
header,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,8 +44,8 @@ section {
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
@@ -60,8 +59,8 @@ h1 {
|
||||
figcaption,
|
||||
figure,
|
||||
main {
|
||||
/* 1 */
|
||||
display: block;
|
||||
/* 1 */
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,7 +68,7 @@ main {
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,9 +77,9 @@ figure {
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,8 +88,8 @@ hr {
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
font-family: monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
@@ -102,8 +101,8 @@ pre {
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent; /* 1 */
|
||||
-webkit-text-decoration-skip: objects; /* 2 */
|
||||
background-color: transparent; /* 1 */
|
||||
text-decoration-skip: objects; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,9 +111,9 @@ a {
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
border-bottom: 0; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +122,7 @@ abbr[title] {
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +131,7 @@ strong {
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,8 +142,8 @@ strong {
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,7 +151,7 @@ samp {
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,8 +159,8 @@ dfn {
|
||||
*/
|
||||
|
||||
mark {
|
||||
background-color: #ff0;
|
||||
color: #000;
|
||||
background-color: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,7 +168,7 @@ mark {
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,18 +178,18 @@ small {
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
@@ -202,7 +201,7 @@ sup {
|
||||
|
||||
audio,
|
||||
video {
|
||||
display: inline-block;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,8 +209,8 @@ video {
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,7 +218,7 @@ audio:not([controls]) {
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,7 +226,7 @@ img {
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
@@ -243,10 +242,10 @@ input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: sans-serif; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
font-family: sans-serif; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -256,8 +255,8 @@ textarea {
|
||||
|
||||
button,
|
||||
input {
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -267,8 +266,8 @@ input {
|
||||
|
||||
button,
|
||||
select {
|
||||
/* 1 */
|
||||
text-transform: none;
|
||||
/* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -281,7 +280,7 @@ button,
|
||||
html [type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
appearance: button; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,8 +291,8 @@ button::-moz-focus-inner,
|
||||
[type='button']::-moz-focus-inner,
|
||||
[type='reset']::-moz-focus-inner,
|
||||
[type='submit']::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,7 +303,7 @@ button:-moz-focusring,
|
||||
[type='button']:-moz-focusring,
|
||||
[type='reset']:-moz-focusring,
|
||||
[type='submit']:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -312,7 +311,7 @@ button:-moz-focusring,
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,12 +322,12 @@ fieldset {
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,8 +336,8 @@ legend {
|
||||
*/
|
||||
|
||||
progress {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,7 +345,7 @@ progress {
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,8 +355,8 @@ textarea {
|
||||
|
||||
[type='checkbox'],
|
||||
[type='radio'] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -366,7 +365,7 @@ textarea {
|
||||
|
||||
[type='number']::-webkit-inner-spin-button,
|
||||
[type='number']::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -375,8 +374,8 @@ textarea {
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -385,7 +384,7 @@ textarea {
|
||||
|
||||
[type='search']::-webkit-search-cancel-button,
|
||||
[type='search']::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -394,8 +393,8 @@ textarea {
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
@@ -408,7 +407,7 @@ textarea {
|
||||
|
||||
details,
|
||||
menu {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -416,7 +415,7 @@ menu {
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Scripting
|
||||
@@ -427,7 +426,7 @@ summary {
|
||||
*/
|
||||
|
||||
canvas {
|
||||
display: inline-block;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -435,7 +434,7 @@ canvas {
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hidden
|
||||
@@ -446,5 +445,5 @@ template {
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
*,
|
||||
*::after,
|
||||
*::before {
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
// ==========================================================================
|
||||
|
||||
// Grayscale
|
||||
$color-gray-9: hsl(210, 15%, 16%);
|
||||
$color-gray-8: lighten($color-gray-9, 9%);
|
||||
$color-gray-7: lighten($color-gray-8, 9%);
|
||||
$color-gray-6: lighten($color-gray-7, 9%);
|
||||
$color-gray-5: lighten($color-gray-6, 9%);
|
||||
$color-gray-4: lighten($color-gray-5, 9%);
|
||||
$color-gray-3: lighten($color-gray-4, 9%);
|
||||
$color-gray-2: lighten($color-gray-3, 9%);
|
||||
$color-gray-1: lighten($color-gray-2, 9%);
|
||||
$color-gray-0: lighten($color-gray-1, 9%);
|
||||
$color-gray-900: hsl(210deg 15% 16%);
|
||||
$color-gray-800: lighten($color-gray-900, 9%);
|
||||
$color-gray-700: lighten($color-gray-800, 9%);
|
||||
$color-gray-600: lighten($color-gray-700, 9%);
|
||||
$color-gray-500: lighten($color-gray-600, 9%);
|
||||
$color-gray-400: lighten($color-gray-500, 9%);
|
||||
$color-gray-300: lighten($color-gray-400, 9%);
|
||||
$color-gray-200: lighten($color-gray-300, 9%);
|
||||
$color-gray-100: lighten($color-gray-200, 9%);
|
||||
$color-gray-50: lighten($color-gray-100, 9%);
|
||||
|
||||
// Branding
|
||||
$color-brand-primary: hsl(198, 100%, 50%);
|
||||
$color-brand-primary: hsl(198deg 100% 50%);
|
||||
|
||||
// Text
|
||||
$color-text: $color-gray-7;
|
||||
$color-text: $color-gray-700;
|
||||
$color-headings: $color-brand-primary;
|
||||
|
||||
// Brands
|
||||
@@ -28,15 +28,15 @@ $color-twitter: #4baaf4;
|
||||
$color-link: $color-brand-primary;
|
||||
|
||||
// Background
|
||||
$color-background-from: hsl(198, 100%, 94%);
|
||||
$color-background-to: hsl(198, 100%, 98%);
|
||||
$color-background-from: hsl(198deg 100% 94%);
|
||||
$color-background-to: hsl(198deg 100% 98%);
|
||||
|
||||
// Buttons
|
||||
$color-button-background: $color-brand-primary;
|
||||
$color-button-text: #fff;
|
||||
$color-button-background-hover: hsl(198, 100%, 55%);
|
||||
$color-button-background-hover: hsl(198deg 100% 55%);
|
||||
$color-button-count-background: #fff;
|
||||
$color-button-count-text: $color-gray-6;
|
||||
$color-button-count-text: $color-gray-600;
|
||||
|
||||
// Focus
|
||||
$tab-focus-default-color: #fff;
|
||||
|
||||
@@ -7,6 +7,7 @@ $arrow-size: 5px;
|
||||
|
||||
// Radii
|
||||
$border-radius-base: 4px;
|
||||
$border-radius-large: 8px;
|
||||
|
||||
// Background
|
||||
$page-background: linear-gradient(to left top, $color-background-from, $color-background-to);
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
// Layout
|
||||
// ==========================================================================
|
||||
|
||||
$container-max-width: 1260px;
|
||||
$container-max-width: 1240px;
|
||||
|
||||
@@ -2,24 +2,17 @@
|
||||
// Plyr Settings
|
||||
// ==========================================================================
|
||||
|
||||
// Font
|
||||
$plyr-font-family: inherit;
|
||||
|
||||
// Sizes
|
||||
$plyr-font-size-base: 13px;
|
||||
$plyr-font-size-small: 12px;
|
||||
$plyr-font-size-time: 11px;
|
||||
$plyr-font-size-badges: 9px;
|
||||
|
||||
// Other
|
||||
$plyr-font-smoothing: true;
|
||||
|
||||
// Colors
|
||||
$plyr-color-main: $color-brand-primary;
|
||||
|
||||
// Captions
|
||||
$plyr-font-size-captions-base: $plyr-font-size-base;
|
||||
$plyr-font-size-captions-small: $plyr-font-size-small;
|
||||
$plyr-font-size-captions-medium: 18px;
|
||||
$plyr-font-size-captions-large: 21px;
|
||||
$plyr-font-size-menu: $plyr-font-size-base;
|
||||
@include css-vars(
|
||||
(
|
||||
--plyr-color-main: $color-brand-primary,
|
||||
--plyr-font-size-base: 13px,
|
||||
--plyr-font-size-small: 12px,
|
||||
--plyr-font-size-time: 11px,
|
||||
--plyr-font-size-badges: 9px,
|
||||
--plyr-font-size-menu: var(--plyr-font-size-base),
|
||||
--plyr-font-weight-regular: 500,
|
||||
--plyr-font-weight-bold: 600,
|
||||
--plyr-font-size-captions-medium: 18px,
|
||||
--plyr-font-size-captions-large: 21px,
|
||||
)
|
||||
);
|
||||
|
||||
@@ -3,19 +3,15 @@
|
||||
// ==========================================================================
|
||||
|
||||
$font-sans-serif: 'Gordita', 'Avenir', 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol';
|
||||
|
||||
'Segoe UI Symbol';
|
||||
$font-size-base: 15;
|
||||
$font-size-small: 13;
|
||||
$font-size-large: 18;
|
||||
$font-size-h1: 64;
|
||||
|
||||
$font-weight-light: 300;
|
||||
$font-weight-regular: 400;
|
||||
$font-weight-medium: 500;
|
||||
$font-weight-bold: 600;
|
||||
$font-weight-black: 900;
|
||||
|
||||
$line-height-base: 1.75;
|
||||
|
||||
$letter-spacing-headings: -0.025em;
|
||||
|
||||
@@ -4,31 +4,33 @@
|
||||
|
||||
// Set to 100% for rem sizing
|
||||
html {
|
||||
font-size: 100%;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
@include font-smoothing();
|
||||
@include font-size($font-size-base);
|
||||
color: $color-text;
|
||||
font-family: $font-sans-serif;
|
||||
font-weight: $font-weight-medium;
|
||||
line-height: $line-height-base;
|
||||
@include font-smoothing;
|
||||
@include font-size($font-size-base);
|
||||
|
||||
color: $color-text;
|
||||
font-family: $font-sans-serif;
|
||||
font-weight: $font-weight-medium;
|
||||
line-height: $line-height-base;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
p,
|
||||
small {
|
||||
margin: 0 0 ($spacing-base * 1.5);
|
||||
margin: 0 0 ($spacing-base * 1.5);
|
||||
}
|
||||
|
||||
small {
|
||||
@include font-size($font-size-small);
|
||||
display: block;
|
||||
@include font-size($font-size-small);
|
||||
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
// ==========================================================================
|
||||
|
||||
h1 {
|
||||
@include font-size($font-size-h1);
|
||||
color: $color-headings;
|
||||
font-weight: $font-weight-bold;
|
||||
letter-spacing: $letter-spacing-headings;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 ($spacing-base * 1.5);
|
||||
@include font-size($font-size-h1);
|
||||
|
||||
color: $color-headings;
|
||||
font-weight: $font-weight-bold;
|
||||
letter-spacing: $letter-spacing-headings;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 ($spacing-base * 1.5);
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
// ==========================================================================
|
||||
|
||||
.no-border {
|
||||
border: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
// ==========================================================================
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Hide only visually, but have it available for screen readers: h5bp.com/v
|
||||
.sr-only {
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
opacity: 0.001;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
opacity: 0.001;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
@@ -2,27 +2,79 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
core-js@^3.1.4:
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.4.tgz#3a2837fc48e582e1ae25907afcd6cf03b0cc7a07"
|
||||
integrity sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==
|
||||
"@sentry/browser@^5.22.3":
|
||||
version "5.22.3"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.22.3.tgz#7a64bd1cf01bf393741a3e4bf35f82aa927f5b4e"
|
||||
integrity sha512-2TzE/CoBa5ZkvxJizDdi1Iz1ldmXSJpFQ1mL07PIXBjCt0Wxf+WOuFSj5IP4L40XHfJE5gU8wEvSH0VDR8nXtA==
|
||||
dependencies:
|
||||
"@sentry/core" "5.22.3"
|
||||
"@sentry/types" "5.22.3"
|
||||
"@sentry/utils" "5.22.3"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/core@5.22.3":
|
||||
version "5.22.3"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.22.3.tgz#030f435f2b518f282ba8bd954dac90cd70888bd7"
|
||||
integrity sha512-eGL5uUarw3o4i9QUb9JoFHnhriPpWCaqeaIBB06HUpdcvhrjoowcKZj1+WPec5lFg5XusE35vez7z/FPzmJUDw==
|
||||
dependencies:
|
||||
"@sentry/hub" "5.22.3"
|
||||
"@sentry/minimal" "5.22.3"
|
||||
"@sentry/types" "5.22.3"
|
||||
"@sentry/utils" "5.22.3"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/hub@5.22.3":
|
||||
version "5.22.3"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.22.3.tgz#08309a70d2ea8d5e313d05840c1711f34f2fffe5"
|
||||
integrity sha512-INo47m6N5HFEs/7GMP9cqxOIt7rmRxdERunA3H2L37owjcr77MwHVeeJ9yawRS6FMtbWXplgWTyTIWIYOuqVbw==
|
||||
dependencies:
|
||||
"@sentry/types" "5.22.3"
|
||||
"@sentry/utils" "5.22.3"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/minimal@5.22.3":
|
||||
version "5.22.3"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.22.3.tgz#706e4029ae5494123d3875c658ba8911aa5cc440"
|
||||
integrity sha512-HoINpYnVYCpNjn2XIPIlqH5o4BAITpTljXjtAftOx6Hzj+Opjg8tR8PWliyKDvkXPpc4kXK9D6TpEDw8MO0wZA==
|
||||
dependencies:
|
||||
"@sentry/hub" "5.22.3"
|
||||
"@sentry/types" "5.22.3"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/types@5.22.3":
|
||||
version "5.22.3"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.22.3.tgz#d1d547b30ee8bd7771fa893af74c4f3d71f0fd18"
|
||||
integrity sha512-cv+VWK0YFgCVDvD1/HrrBWOWYG3MLuCUJRBTkV/Opdy7nkdNjhCAJQrEyMM9zX0sac8FKWKOHT0sykNh8KgmYw==
|
||||
|
||||
"@sentry/utils@5.22.3":
|
||||
version "5.22.3"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.22.3.tgz#e3bda3e789239eb16d436f768daa12829f33d18f"
|
||||
integrity sha512-AHNryXMBvIkIE+GQxTlmhBXD0Ksh+5w1SwM5qi6AttH+1qjWLvV6WB4+4pvVvEoS8t5F+WaVUZPQLmCCWp6zKw==
|
||||
dependencies:
|
||||
"@sentry/types" "5.22.3"
|
||||
tslib "^1.9.3"
|
||||
|
||||
core-js@^3.6.5:
|
||||
version "3.6.5"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
|
||||
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
|
||||
|
||||
custom-event-polyfill@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee"
|
||||
integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
|
||||
|
||||
raven-js@^3.27.2:
|
||||
version "3.27.2"
|
||||
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.27.2.tgz#6c33df952026cd73820aa999122b7b7737a66775"
|
||||
integrity sha512-mFWQcXnhRFEQe5HeFroPaEghlnqy7F5E2J3Fsab189ondqUzcjwSVi7el7F36cr6PvQYXoZ1P2F5CSF2/azeMQ==
|
||||
|
||||
shr-buttons@2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/shr-buttons/-/shr-buttons-2.0.3.tgz#2ffd021fc3d789e1510ce2736b938bd09ea1da5a"
|
||||
integrity sha512-sPAgHiw4uaIt9TnxTfyZEedDChcldSVtnBHE44cpe/mSC7rqm4IEKZRLYqnVlTcGM+FSDNBPUNpSf50Q2ntd+w==
|
||||
|
||||
url-polyfill@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.5.tgz#bec79b72b5407dba6d8cced2e32e4ab273aa9fb1"
|
||||
integrity sha512-9XjIJ6nwrU+nGd8t90Ze0Zs7t8A+SU0gqsqPttj6j3zAVe5q0HFcuv37nDBdVSPpi4aTHTfbUF/i+ZVD+o2EbA==
|
||||
tslib@^1.9.3:
|
||||
version "1.11.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
|
||||
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
|
||||
|
||||
url-polyfill@^1.1.10:
|
||||
version "1.1.10"
|
||||
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.10.tgz#fd5bbcf66c9491fa682b0cb6d6a855e1643a9281"
|
||||
integrity sha512-vSaPpaRgBrf41+Uky1myiSh6gpcbw8FpwHYnEy0abxndojOBnIs+yh/49gKYFLtUMP9qoNWjn6j9aUVy23Ie2A==
|
||||
|
||||
20
deploy.json
20
deploy.json
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"cdn": {
|
||||
"bucket": "plyr",
|
||||
"domain": "cdn.plyr.io",
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"demo": {
|
||||
"bucket": "plyr.io",
|
||||
"domain": "plyr.io",
|
||||
"region": "us-west-1"
|
||||
}
|
||||
"cdn": {
|
||||
"bucket": "plyr",
|
||||
"domain": "cdn.plyr.io",
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"demo": {
|
||||
"bucket": "plyr.io",
|
||||
"domain": "plyr.io",
|
||||
"region": "us-west-1"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
dist/blank.mp4
vendored
BIN
dist/blank.mp4
vendored
Binary file not shown.
1
dist/plyr.css
vendored
1
dist/plyr.css
vendored
File diff suppressed because one or more lines are too long
9252
dist/plyr.js
vendored
9252
dist/plyr.js
vendored
File diff suppressed because it is too large
Load Diff
4
dist/plyr.min.js
vendored
4
dist/plyr.min.js
vendored
File diff suppressed because one or more lines are too long
1
dist/plyr.min.js.map
vendored
1
dist/plyr.min.js.map
vendored
File diff suppressed because one or more lines are too long
3
dist/plyr.min.mjs
vendored
3
dist/plyr.min.mjs
vendored
File diff suppressed because one or more lines are too long
1
dist/plyr.min.mjs.map
vendored
1
dist/plyr.min.mjs.map
vendored
File diff suppressed because one or more lines are too long
9244
dist/plyr.mjs
vendored
9244
dist/plyr.mjs
vendored
File diff suppressed because it is too large
Load Diff
15650
dist/plyr.polyfilled.js
vendored
15650
dist/plyr.polyfilled.js
vendored
File diff suppressed because it is too large
Load Diff
4
dist/plyr.polyfilled.min.js
vendored
4
dist/plyr.polyfilled.min.js
vendored
File diff suppressed because one or more lines are too long
1
dist/plyr.polyfilled.min.js.map
vendored
1
dist/plyr.polyfilled.min.js.map
vendored
File diff suppressed because one or more lines are too long
3
dist/plyr.polyfilled.min.mjs
vendored
3
dist/plyr.polyfilled.min.mjs
vendored
File diff suppressed because one or more lines are too long
1
dist/plyr.polyfilled.min.mjs.map
vendored
1
dist/plyr.polyfilled.min.mjs.map
vendored
File diff suppressed because one or more lines are too long
15642
dist/plyr.polyfilled.mjs
vendored
15642
dist/plyr.polyfilled.mjs
vendored
File diff suppressed because it is too large
Load Diff
1
dist/plyr.svg
vendored
1
dist/plyr.svg
vendored
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 5.6 KiB |
518
gulpfile.js
518
gulpfile.js
@@ -1,522 +1,8 @@
|
||||
// ==========================================================================
|
||||
// Gulp build script
|
||||
// ==========================================================================
|
||||
/* eslint no-console: "off" */
|
||||
|
||||
const path = require('path');
|
||||
const gulp = require('gulp');
|
||||
// ------------------------------------
|
||||
// JavaScript
|
||||
// ------------------------------------
|
||||
const terser = require('gulp-terser');
|
||||
const rollup = require('gulp-better-rollup');
|
||||
const babel = require('rollup-plugin-babel');
|
||||
const commonjs = require('rollup-plugin-commonjs');
|
||||
const resolve = require('rollup-plugin-node-resolve');
|
||||
// ------------------------------------
|
||||
// CSS
|
||||
// ------------------------------------
|
||||
const sass = require('gulp-sass');
|
||||
const clean = require('gulp-clean-css');
|
||||
const prefix = require('gulp-autoprefixer');
|
||||
// ------------------------------------
|
||||
// Images
|
||||
// ------------------------------------
|
||||
const svgstore = require('gulp-svgstore');
|
||||
const imagemin = require('gulp-imagemin');
|
||||
// ------------------------------------
|
||||
// Utils
|
||||
// ------------------------------------
|
||||
const del = require('del');
|
||||
const filter = require('gulp-filter');
|
||||
const header = require('gulp-header');
|
||||
const gitbranch = require('git-branch');
|
||||
const rename = require('gulp-rename');
|
||||
const replace = require('gulp-replace');
|
||||
const ansi = require('ansi-colors');
|
||||
const log = require('fancy-log');
|
||||
const open = require('gulp-open');
|
||||
const plumber = require('gulp-plumber');
|
||||
const size = require('gulp-size');
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
const through = require('through2');
|
||||
const browserSync = require('browser-sync').create();
|
||||
// ------------------------------------
|
||||
// Deployment
|
||||
// ------------------------------------
|
||||
const aws = require('aws-sdk');
|
||||
const publish = require('gulp-awspublish');
|
||||
const FastlyPurge = require('fastly-purge');
|
||||
// ------------------------------------
|
||||
// Configs
|
||||
// ------------------------------------
|
||||
const pkg = require('./package.json');
|
||||
const build = require('./build.json');
|
||||
const deploy = require('./deploy.json');
|
||||
// ------------------------------------
|
||||
// Info from package
|
||||
// ------------------------------------
|
||||
const { browserslist: browsers, version } = pkg;
|
||||
const minSuffix = '.min';
|
||||
const HubRegistry = require('gulp-hub');
|
||||
|
||||
// Get AWS config
|
||||
Object.values(deploy).forEach(target => {
|
||||
Object.assign(target, {
|
||||
publisher: publish.create({
|
||||
region: target.region,
|
||||
params: {
|
||||
Bucket: target.bucket,
|
||||
},
|
||||
credentials: new aws.SharedIniFileCredentials({ profile: 'plyr' }),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Paths
|
||||
const paths = {
|
||||
plyr: {
|
||||
// Source paths
|
||||
src: {
|
||||
sass: path.join(__dirname, 'src/sass/**/*.scss'),
|
||||
js: path.join(__dirname, 'src/js/**/*.js'),
|
||||
sprite: path.join(__dirname, 'src/sprite/*.svg'),
|
||||
},
|
||||
|
||||
// Output paths
|
||||
output: path.join(__dirname, 'dist/'),
|
||||
},
|
||||
demo: {
|
||||
// Source paths
|
||||
src: {
|
||||
sass: path.join(__dirname, 'demo/src/sass/**/*.scss'),
|
||||
js: path.join(__dirname, 'demo/src/js/**/*.js'),
|
||||
},
|
||||
|
||||
// Output paths
|
||||
output: path.join(__dirname, 'demo/dist/'),
|
||||
|
||||
// Demo
|
||||
root: path.join(__dirname, 'demo/'),
|
||||
},
|
||||
upload: [
|
||||
path.join(__dirname, `dist/*${minSuffix}.*`),
|
||||
path.join(__dirname, 'dist/*.css'),
|
||||
path.join(__dirname, 'dist/*.svg'),
|
||||
path.join(__dirname, `demo/dist/*${minSuffix}.*`),
|
||||
path.join(__dirname, 'demo/dist/*.css'),
|
||||
path.join(__dirname, 'demo/dist/*.svg'),
|
||||
],
|
||||
};
|
||||
|
||||
// Task arrays
|
||||
const tasks = {
|
||||
css: [],
|
||||
js: [],
|
||||
sprite: [],
|
||||
clean: 'clean',
|
||||
};
|
||||
|
||||
// Size plugin
|
||||
const sizeOptions = { showFiles: true, gzip: true };
|
||||
|
||||
// Clean out /dist
|
||||
gulp.task(tasks.clean, done => {
|
||||
const dirs = [paths.plyr.output, paths.demo.output].map(dir => path.join(dir, '**/*'));
|
||||
|
||||
// Don't delete the mp4
|
||||
dirs.push(`!${path.join(paths.plyr.output, '**/*.mp4')}`);
|
||||
|
||||
del(dirs);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
// JavaScript
|
||||
Object.entries(build.js).forEach(([filename, entry]) => {
|
||||
const { dist, formats, namespace, polyfill, src } = entry;
|
||||
|
||||
formats.forEach(format => {
|
||||
const name = `js:${filename}:${format}`;
|
||||
const extension = format === 'es' ? 'mjs' : 'js';
|
||||
tasks.js.push(name);
|
||||
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(src)
|
||||
.pipe(plumber())
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(
|
||||
rollup(
|
||||
{
|
||||
plugins: [
|
||||
resolve(),
|
||||
commonjs(),
|
||||
babel({
|
||||
presets: [
|
||||
[
|
||||
'@babel/env',
|
||||
{
|
||||
// debug: true,
|
||||
useBuiltIns: polyfill ? 'usage' : false,
|
||||
corejs: polyfill ? 3 : undefined,
|
||||
},
|
||||
],
|
||||
],
|
||||
babelrc: false,
|
||||
exclude: [/\/core-js\//],
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: namespace,
|
||||
format,
|
||||
},
|
||||
),
|
||||
)
|
||||
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
|
||||
.pipe(
|
||||
rename({
|
||||
extname: `.${extension}`,
|
||||
}),
|
||||
)
|
||||
.pipe(gulp.dest(dist))
|
||||
.pipe(filter(`**/*.${extension}`))
|
||||
.pipe(terser())
|
||||
.pipe(rename({ suffix: minSuffix }))
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(sourcemaps.write(''))
|
||||
.pipe(gulp.dest(dist)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// CSS
|
||||
Object.entries(build.css).forEach(([filename, entry]) => {
|
||||
const { dist, src } = entry;
|
||||
const name = `css:${filename}`;
|
||||
tasks.css.push(name);
|
||||
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(src)
|
||||
.pipe(plumber())
|
||||
.pipe(sass())
|
||||
.pipe(
|
||||
prefix(browsers, {
|
||||
cascade: false,
|
||||
}),
|
||||
)
|
||||
.pipe(clean())
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(gulp.dest(dist)),
|
||||
);
|
||||
});
|
||||
|
||||
// SVG Sprites
|
||||
Object.entries(build.sprite).forEach(([filename, entry]) => {
|
||||
const { dist, src } = entry;
|
||||
const name = `sprite:${filename}`;
|
||||
tasks.sprite.push(name);
|
||||
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(src)
|
||||
.pipe(plumber())
|
||||
.pipe(
|
||||
imagemin([
|
||||
imagemin.svgo({
|
||||
plugins: [{ removeViewBox: false }],
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.pipe(svgstore())
|
||||
.pipe(rename({ basename: path.parse(filename).name }))
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(gulp.dest(dist)),
|
||||
);
|
||||
});
|
||||
|
||||
// Build all JS
|
||||
gulp.task('js', () => gulp.parallel(...tasks.js));
|
||||
|
||||
// Watch for file changes
|
||||
gulp.task('watch', () => {
|
||||
// Plyr core
|
||||
gulp.watch(paths.plyr.src.js, gulp.parallel(...tasks.js));
|
||||
gulp.watch(paths.plyr.src.sass, gulp.parallel(...tasks.css));
|
||||
gulp.watch(paths.plyr.src.sprite, gulp.parallel(...tasks.sprite));
|
||||
|
||||
// Demo
|
||||
gulp.watch(paths.demo.src.js, gulp.parallel(...tasks.js));
|
||||
gulp.watch(paths.demo.src.sass, gulp.parallel(...tasks.css));
|
||||
});
|
||||
|
||||
// Serve via browser sync
|
||||
gulp.task('serve', () =>
|
||||
browserSync.init({
|
||||
server: {
|
||||
baseDir: paths.demo.root,
|
||||
},
|
||||
notify: false,
|
||||
watch: true,
|
||||
ghostMode: false,
|
||||
}),
|
||||
);
|
||||
|
||||
// Build distribution
|
||||
gulp.task('build', gulp.series(tasks.clean, gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite)));
|
||||
|
||||
// Default gulp task
|
||||
gulp.task('default', gulp.series('build', gulp.parallel('serve', 'watch')));
|
||||
|
||||
// Publish a version to CDN and demo
|
||||
// --------------------------------------------
|
||||
// Get deployment config
|
||||
let credentials = {};
|
||||
try {
|
||||
credentials = require('./credentials.json'); //eslint-disable-line
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
// Get branch info
|
||||
const branch = {
|
||||
current: gitbranch.sync(),
|
||||
master: 'master',
|
||||
beta: 'beta',
|
||||
};
|
||||
|
||||
const maxAge = 31536000; // 1 year
|
||||
const options = {
|
||||
cdn: {
|
||||
headers: {
|
||||
'Cache-Control': `max-age=${maxAge}`,
|
||||
},
|
||||
},
|
||||
demo: {
|
||||
uploadPath: branch.current === branch.beta ? '/beta' : null,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
|
||||
},
|
||||
},
|
||||
symlinks(ver, filename) {
|
||||
return {
|
||||
headers: {
|
||||
// http://stackoverflow.com/questions/2272835/amazon-s3-object-redirect
|
||||
'x-amz-website-redirect-location': `/${ver}/${filename}`,
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const regex =
|
||||
'(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*).(?:0|[1-9][0-9]*)(?:-[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?(?:\\+[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?';
|
||||
const semver = new RegExp(`v${regex}`, 'gi');
|
||||
const localPath = new RegExp('(../)?dist', 'gi');
|
||||
const versionPath = `https://${deploy.cdn.domain}/${version}`;
|
||||
const cdnpath = new RegExp(`${deploy.cdn.domain}/${regex}/`, 'gi');
|
||||
|
||||
const renameFile = rename(p => {
|
||||
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
|
||||
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
|
||||
});
|
||||
|
||||
// Check we're on the correct branch to deploy
|
||||
const canDeploy = () => {
|
||||
const allowed = [branch.master, branch.beta];
|
||||
|
||||
if (!allowed.includes(branch.current)) {
|
||||
console.error(`Must be on ${allowed.join(', ')} to publish! (current: ${branch.current})`);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
gulp.task('version', done => {
|
||||
if (!canDeploy()) {
|
||||
done();
|
||||
return null;
|
||||
}
|
||||
|
||||
const { domain } = deploy.cdn;
|
||||
|
||||
log(`Uploading ${ansi.green.bold(version)} to ${ansi.cyan(domain)}...`);
|
||||
|
||||
// Replace versioned URLs in source
|
||||
const files = ['plyr.js', 'plyr.polyfilled.js', 'config/defaults.js'];
|
||||
|
||||
return gulp
|
||||
.src(
|
||||
files.map(file => path.join(__dirname, `src/js/${file}`)),
|
||||
{ base: '.' },
|
||||
)
|
||||
.pipe(replace(semver, `v${version}`))
|
||||
.pipe(replace(cdnpath, `${domain}/${version}/`))
|
||||
.pipe(gulp.dest('./'));
|
||||
});
|
||||
|
||||
// Publish version to CDN bucket
|
||||
gulp.task('cdn', done => {
|
||||
if (!canDeploy()) {
|
||||
done();
|
||||
return null;
|
||||
}
|
||||
|
||||
const { domain, publisher } = deploy.cdn;
|
||||
|
||||
if (!publisher) {
|
||||
throw new Error('No publisher instance. Check AWS configuration.');
|
||||
}
|
||||
|
||||
log(`Uploading ${ansi.green.bold(pkg.version)} to ${ansi.cyan(domain)}...`);
|
||||
|
||||
// Upload to CDN
|
||||
return (
|
||||
gulp
|
||||
.src(paths.upload)
|
||||
.pipe(renameFile)
|
||||
// Remove min suffix from source map URL
|
||||
.pipe(
|
||||
replace(
|
||||
/sourceMappingURL=([\w-?.]+)/,
|
||||
(match, filename) => `sourceMappingURL=${filename.replace(minSuffix, '')}`,
|
||||
),
|
||||
)
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(replace(localPath, versionPath))
|
||||
.pipe(publisher.publish(options.cdn.headers))
|
||||
.pipe(publish.reporter())
|
||||
);
|
||||
});
|
||||
|
||||
// Purge the fastly cache incase any 403/404 are cached
|
||||
gulp.task('purge', () => {
|
||||
if (!Object.keys(credentials).includes('fastly')) {
|
||||
throw new Error('Fastly credentials required to purge cache.');
|
||||
}
|
||||
|
||||
const { fastly } = credentials;
|
||||
const list = [];
|
||||
|
||||
return gulp
|
||||
.src(paths.upload)
|
||||
.pipe(
|
||||
through.obj((file, enc, cb) => {
|
||||
const filename = file.path.split('/').pop();
|
||||
list.push(`${versionPath}/${filename.replace(minSuffix, '')}`);
|
||||
cb(null);
|
||||
}),
|
||||
)
|
||||
.on('end', () => {
|
||||
const purge = new FastlyPurge(fastly.token);
|
||||
|
||||
list.forEach(url => {
|
||||
log(`Purging ${ansi.cyan(url)}...`);
|
||||
|
||||
purge.url(url, (error, result) => {
|
||||
if (error) {
|
||||
log.error(error);
|
||||
} else if (result) {
|
||||
log(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Publish to demo bucket
|
||||
gulp.task('demo', done => {
|
||||
if (!canDeploy()) {
|
||||
done();
|
||||
return null;
|
||||
}
|
||||
|
||||
const { publisher } = deploy.demo;
|
||||
const { domain } = deploy.cdn;
|
||||
|
||||
if (!publisher) {
|
||||
throw new Error('No publisher instance. Check AWS configuration.');
|
||||
}
|
||||
|
||||
log(`Uploading ${ansi.green.bold(pkg.version)} to ${ansi.cyan(domain)}...`);
|
||||
|
||||
// Replace versioned files in readme.md
|
||||
gulp.src([`${__dirname}/readme.md`])
|
||||
.pipe(replace(cdnpath, `${domain}/${version}/`))
|
||||
.pipe(gulp.dest(__dirname));
|
||||
|
||||
// Replace local file paths with remote paths in demo HTML
|
||||
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
|
||||
const index = `${paths.demo.root}index.html`;
|
||||
const error = `${paths.demo.root}error.html`;
|
||||
const pages = [index];
|
||||
|
||||
if (branch.current === branch.master) {
|
||||
pages.push(error);
|
||||
}
|
||||
|
||||
return gulp
|
||||
.src(pages)
|
||||
.pipe(replace(localPath, versionPath))
|
||||
.pipe(
|
||||
rename(p => {
|
||||
if (options.demo.uploadPath) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
p.dirname += options.demo.uploadPath;
|
||||
}
|
||||
}),
|
||||
)
|
||||
.pipe(publisher.publish(options.demo.headers))
|
||||
.pipe(publish.reporter());
|
||||
});
|
||||
|
||||
gulp.task('error', done => {
|
||||
// Only update CDN for master (prod)
|
||||
if (!canDeploy() || branch.current !== branch.master) {
|
||||
done();
|
||||
return null;
|
||||
}
|
||||
|
||||
const { publisher } = deploy.cdn;
|
||||
|
||||
if (!publisher) {
|
||||
throw new Error('No publisher instance. Check AWS configuration.');
|
||||
}
|
||||
|
||||
// Replace local file paths with remote paths in demo HTML
|
||||
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
|
||||
// Upload error.html to cdn
|
||||
return gulp
|
||||
.src(`${paths.demo.root}error.html`)
|
||||
.pipe(replace(localPath, versionPath))
|
||||
.pipe(publisher.publish(options.demo.headers))
|
||||
.pipe(publish.reporter());
|
||||
});
|
||||
|
||||
// Open the demo site to check it's ok
|
||||
gulp.task('open', () => {
|
||||
const { domain } = deploy.demo;
|
||||
|
||||
return gulp.src(__filename).pipe(
|
||||
open({
|
||||
uri: `https://${domain}/${branch.current === branch.beta ? 'beta' : ''}`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// Do everything
|
||||
gulp.task(
|
||||
'deploy',
|
||||
gulp.series(
|
||||
'version',
|
||||
tasks.clean,
|
||||
gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite),
|
||||
'cdn',
|
||||
'demo',
|
||||
'purge',
|
||||
'open',
|
||||
),
|
||||
);
|
||||
gulp.registry(new HubRegistry(['tasks/*.js']));
|
||||
|
||||
194
package.json
194
package.json
@@ -1,98 +1,100 @@
|
||||
{
|
||||
"name": "plyr",
|
||||
"version": "3.5.10",
|
||||
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
||||
"homepage": "https://plyr.io",
|
||||
"author": "Sam Potts <sam@potts.es>",
|
||||
"main": "dist/plyr.js",
|
||||
"types": "src/js/plyr.d.ts",
|
||||
"module": "dist/plyr.min.mjs",
|
||||
"jsnext:main": "dist/plyr.min.mjs",
|
||||
"browser": "dist/plyr.min.js",
|
||||
"sass": "src/sass/plyr.scss",
|
||||
"style": "dist/plyr.css",
|
||||
"keywords": [
|
||||
"HTML5 Video",
|
||||
"HTML5 Audio",
|
||||
"Media Player",
|
||||
"DASH",
|
||||
"Shaka",
|
||||
"WordPress",
|
||||
"HLS"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/sampotts/plyr.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/sampotts/plyr/issues"
|
||||
},
|
||||
"browserslist": "> 1%",
|
||||
"scripts": {
|
||||
"build": "gulp build",
|
||||
"lint": "eslint src/js && npm run-script remark",
|
||||
"lint:fix": "eslint --fix src/js",
|
||||
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
|
||||
"deploy": "yarn lint && gulp deploy"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ansi-colors": "^4.1.1",
|
||||
"aws-sdk": "^2.614.0",
|
||||
"@babel/core": "^7.8.4",
|
||||
"@babel/preset-env": "^7.8.4",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"browser-sync": "^2.26.7",
|
||||
"del": "^5.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb-base": "^14.0.0",
|
||||
"eslint-config-prettier": "^6.10.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-simple-import-sort": "^5.0.1",
|
||||
"fancy-log": "^1.3.3",
|
||||
"fastly-purge": "^1.0.1",
|
||||
"git-branch": "^2.0.1",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-autoprefixer": "^7.0.1",
|
||||
"gulp-awspublish": "^4.1.1",
|
||||
"gulp-better-rollup": "^4.0.1",
|
||||
"gulp-clean-css": "^4.2.0",
|
||||
"gulp-filter": "^6.0.0",
|
||||
"gulp-header": "^2.0.9",
|
||||
"gulp-imagemin": "^7.1.0",
|
||||
"gulp-open": "^3.0.1",
|
||||
"gulp-plumber": "^1.2.1",
|
||||
"gulp-postcss": "^8.0.0",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-replace": "^1.0.0",
|
||||
"gulp-sass": "^4.0.2",
|
||||
"gulp-size": "^3.0.0",
|
||||
"gulp-sourcemaps": "^2.6.5",
|
||||
"gulp-svgstore": "^7.0.1",
|
||||
"gulp-terser": "^1.2.0",
|
||||
"postcss-custom-properties": "^9.0.2",
|
||||
"prettier-eslint": "^9.0.1",
|
||||
"prettier-stylelint": "^0.4.2",
|
||||
"remark-cli": "^7.0.1",
|
||||
"remark-validate-links": "^9.2.0",
|
||||
"rollup": "^1.31.0",
|
||||
"rollup-plugin-babel": "^4.3.3",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"stylelint": "^13.1.0",
|
||||
"stylelint-config-prettier": "^8.0.1",
|
||||
"stylelint-config-recommended": "^3.0.0",
|
||||
"stylelint-config-sass-guidelines": "^7.0.0",
|
||||
"stylelint-order": "^4.0.0",
|
||||
"stylelint-scss": "^3.14.2",
|
||||
"stylelint-selector-bem-pattern": "^2.1.0",
|
||||
"through2": "^3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.4",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"loadjs": "^4.2.0",
|
||||
"rangetouch": "^2.0.0",
|
||||
"url-polyfill": "^1.1.8"
|
||||
}
|
||||
"name": "plyr",
|
||||
"version": "3.7.0",
|
||||
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
||||
"homepage": "https://plyr.io",
|
||||
"author": "Sam Potts <sam@potts.es>",
|
||||
"main": "dist/plyr.js",
|
||||
"types": "src/js/plyr.d.ts",
|
||||
"module": "dist/plyr.min.mjs",
|
||||
"jsnext:main": "dist/plyr.min.mjs",
|
||||
"browser": "dist/plyr.min.js",
|
||||
"sass": "src/sass/plyr.scss",
|
||||
"style": "dist/plyr.css",
|
||||
"keywords": [
|
||||
"HTML5 Video",
|
||||
"HTML5 Audio",
|
||||
"Media Player",
|
||||
"DASH",
|
||||
"Shaka",
|
||||
"WordPress",
|
||||
"HLS"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/sampotts/plyr.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/sampotts/plyr/issues"
|
||||
},
|
||||
"browserslist": "> 1%",
|
||||
"scripts": {
|
||||
"build": "gulp build",
|
||||
"lint": "eslint src/js && npm run remark && stylelint **/*.scss",
|
||||
"lint:fix": "eslint --fix src/js && stylelint **/*.scss --fix",
|
||||
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
|
||||
"deploy": "yarn lint && gulp version && gulp build && gulp deploy",
|
||||
"format": "prettier --write \"./{src,demo/src}/**/*.{js,scss}\"",
|
||||
"spellcheck": "cspell \"**/*.{js,md,scss,json}\" --no-must-find-files",
|
||||
"start": "gulp"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@sampotts/eslint-config": "1.1.7",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"aws-sdk": "^2.1116.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"browser-sync": "^2.27.9",
|
||||
"colorette": "2.0.16",
|
||||
"cspell": "^5.19.7",
|
||||
"cssnano": "^5.1.7",
|
||||
"del": "^6.0.0",
|
||||
"eslint": "^7.23.0",
|
||||
"fancy-log": "^2.0.0",
|
||||
"git-branch": "^2.0.1",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-awspublish": "^6.0.2",
|
||||
"gulp-better-rollup": "^4.0.1",
|
||||
"gulp-filter": "^7.0.0",
|
||||
"gulp-header": "^2.0.9",
|
||||
"gulp-hub": "^4.2.0",
|
||||
"gulp-if": "^3.0.0",
|
||||
"gulp-imagemin": "^7.1.0",
|
||||
"gulp-open": "^3.0.1",
|
||||
"gulp-plumber": "^1.2.1",
|
||||
"gulp-postcss": "^9.0.1",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-replace": "^1.1.3",
|
||||
"gulp-sass": "^5.1.0",
|
||||
"gulp-size": "^4.0.1",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-svgstore": "^9.0.0",
|
||||
"gulp-terser": "^2.1.0",
|
||||
"postcss": "^8.4.12",
|
||||
"postcss-custom-properties": "^12.1.7",
|
||||
"postcss-scss": "^4.0.3",
|
||||
"prettier-eslint": "^12.0.0",
|
||||
"prettier-stylelint": "^0.4.2",
|
||||
"remark-cli": "^10.0.1",
|
||||
"remark-validate-links": "^11.0.2",
|
||||
"rollup": "^2.70.2",
|
||||
"rollup-plugin-babel": "^4.4.0",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"sass": "^1.50.0",
|
||||
"stylelint": "^14.7.1",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-sass-guidelines": "^9.0.1",
|
||||
"stylelint-selector-bem-pattern": "^2.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.22.0",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"loadjs": "^4.2.0",
|
||||
"rangetouch": "^2.0.1",
|
||||
"url-polyfill": "^1.1.12"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/dist": true
|
||||
},
|
||||
|
||||
// Linting
|
||||
"stylelint.enable": true,
|
||||
"css.validate": false,
|
||||
"scss.validate": false,
|
||||
"javascript.validate.enable": false,
|
||||
|
||||
// Formatting
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.tabSize": 4,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.formatOnSave": true,
|
||||
|
||||
// Trim on save
|
||||
"files.trimTrailingWhitespace": true,
|
||||
|
||||
// Special file associations
|
||||
"files.associations": {
|
||||
".eslintrc": "jsonc"
|
||||
},
|
||||
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
}
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/dist": true
|
||||
},
|
||||
|
||||
// Linting
|
||||
"stylelint.enable": true,
|
||||
"stylelint.validate": ["css", "scss"],
|
||||
"css.validate": false,
|
||||
"less.validate": false,
|
||||
"scss.validate": false,
|
||||
"javascript.validate.enable": false,
|
||||
|
||||
// Formatting
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.formatOnSave": true,
|
||||
|
||||
// Trim on save
|
||||
"files.trimTrailingWhitespace": true,
|
||||
|
||||
// Special file associations
|
||||
"files.associations": {
|
||||
".eslintrc": "jsonc"
|
||||
},
|
||||
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ import support from './support';
|
||||
import { dedupe } from './utils/arrays';
|
||||
import browser from './utils/browser';
|
||||
import {
|
||||
createElement,
|
||||
emptyElement,
|
||||
getAttributesFromSelector,
|
||||
insertAfter,
|
||||
removeElement,
|
||||
toggleClass,
|
||||
createElement,
|
||||
emptyElement,
|
||||
getAttributesFromSelector,
|
||||
insertAfter,
|
||||
removeElement,
|
||||
toggleClass,
|
||||
} from './utils/elements';
|
||||
import { on, triggerEvent } from './utils/events';
|
||||
import fetch from './utils/fetch';
|
||||
@@ -23,367 +23,387 @@ import { getHTML } from './utils/strings';
|
||||
import { parseUrl } from './utils/urls';
|
||||
|
||||
const captions = {
|
||||
// Setup captions
|
||||
setup() {
|
||||
// Requires UI support
|
||||
if (!this.supported.ui) {
|
||||
return;
|
||||
}
|
||||
// Setup captions
|
||||
setup() {
|
||||
// Requires UI support
|
||||
if (!this.supported.ui) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only Vimeo and HTML5 video supported at this point
|
||||
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
|
||||
// Clear menu and hide
|
||||
if (
|
||||
is.array(this.config.controls) &&
|
||||
this.config.controls.includes('settings') &&
|
||||
this.config.settings.includes('captions')
|
||||
) {
|
||||
controls.setCaptionsMenu.call(this);
|
||||
}
|
||||
// Only Vimeo and HTML5 video supported at this point
|
||||
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
|
||||
// Clear menu and hide
|
||||
if (
|
||||
is.array(this.config.controls) &&
|
||||
this.config.controls.includes('settings') &&
|
||||
this.config.settings.includes('captions')
|
||||
) {
|
||||
controls.setCaptionsMenu.call(this);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject the container
|
||||
if (!is.element(this.elements.captions)) {
|
||||
this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
|
||||
// Inject the container
|
||||
if (!is.element(this.elements.captions)) {
|
||||
this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
|
||||
|
||||
insertAfter(this.elements.captions, this.elements.wrapper);
|
||||
}
|
||||
insertAfter(this.elements.captions, this.elements.wrapper);
|
||||
}
|
||||
|
||||
// Fix IE captions if CORS is used
|
||||
// Fetch captions and inject as blobs instead (data URIs not supported!)
|
||||
if (browser.isIE && window.URL) {
|
||||
const elements = this.media.querySelectorAll('track');
|
||||
// Fix IE captions if CORS is used
|
||||
// Fetch captions and inject as blobs instead (data URIs not supported!)
|
||||
if (browser.isIE && window.URL) {
|
||||
const elements = this.media.querySelectorAll('track');
|
||||
|
||||
Array.from(elements).forEach(track => {
|
||||
const src = track.getAttribute('src');
|
||||
const url = parseUrl(src);
|
||||
Array.from(elements).forEach((track) => {
|
||||
const src = track.getAttribute('src');
|
||||
const url = parseUrl(src);
|
||||
|
||||
if (
|
||||
url !== null &&
|
||||
url.hostname !== window.location.href.hostname &&
|
||||
['http:', 'https:'].includes(url.protocol)
|
||||
) {
|
||||
fetch(src, 'blob')
|
||||
.then(blob => {
|
||||
track.setAttribute('src', window.URL.createObjectURL(blob));
|
||||
})
|
||||
.catch(() => {
|
||||
removeElement(track);
|
||||
});
|
||||
}
|
||||
if (
|
||||
url !== null &&
|
||||
url.hostname !== window.location.href.hostname &&
|
||||
['http:', 'https:'].includes(url.protocol)
|
||||
) {
|
||||
fetch(src, 'blob')
|
||||
.then((blob) => {
|
||||
track.setAttribute('src', window.URL.createObjectURL(blob));
|
||||
})
|
||||
.catch(() => {
|
||||
removeElement(track);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get and set initial data
|
||||
// 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
|
||||
// Get and set initial data
|
||||
// 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
|
||||
|
||||
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
|
||||
const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));
|
||||
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
|
||||
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
|
||||
const languages = dedupe(browserLanguages.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;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
let active = this.storage.get('captions');
|
||||
if (!is.boolean(active)) {
|
||||
({ active } = this.config.captions);
|
||||
}
|
||||
|
||||
Object.assign(this.captions, {
|
||||
toggled: false,
|
||||
active,
|
||||
language,
|
||||
languages,
|
||||
Object.assign(this.captions, {
|
||||
toggled: false,
|
||||
active,
|
||||
language,
|
||||
languages,
|
||||
});
|
||||
|
||||
// Watch changes to textTracks and update captions menu
|
||||
if (this.isHTML5) {
|
||||
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
|
||||
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)
|
||||
setTimeout(captions.update.bind(this), 0);
|
||||
},
|
||||
|
||||
// Update available language options in settings based on tracks
|
||||
update() {
|
||||
const tracks = captions.getTracks.call(this, true);
|
||||
// Get the wanted language
|
||||
const { active, language, meta, currentTrackNode } = this.captions;
|
||||
const languageExists = Boolean(tracks.find((track) => track.language === language));
|
||||
|
||||
// Handle tracks (add event listener and "pseudo"-default)
|
||||
if (this.isHTML5 && this.isVideo) {
|
||||
tracks
|
||||
.filter((track) => !meta.get(track))
|
||||
.forEach((track) => {
|
||||
this.debug.log('Track added', track);
|
||||
|
||||
// Attempt to store if the original dom element was "default"
|
||||
meta.set(track, {
|
||||
default: track.mode === 'showing',
|
||||
});
|
||||
|
||||
// Turn off native caption rendering to avoid double captions
|
||||
// Note: mode='hidden' forces a track to download. To ensure every track
|
||||
// isn't downloaded at once, only 'showing' tracks should be reassigned
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (track.mode === 'showing') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
track.mode = 'hidden';
|
||||
}
|
||||
|
||||
// Add event listener for cue changes
|
||||
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
|
||||
});
|
||||
}
|
||||
|
||||
// Watch changes to textTracks and update captions menu
|
||||
if (this.isHTML5) {
|
||||
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
|
||||
on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
|
||||
}
|
||||
// Update language first time it matches, or if the previous matching track was removed
|
||||
if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
|
||||
captions.setLanguage.call(this, language);
|
||||
captions.toggle.call(this, active && languageExists);
|
||||
}
|
||||
|
||||
// Update available languages in list next tick (the event must not be triggered before the listeners)
|
||||
setTimeout(captions.update.bind(this), 0);
|
||||
},
|
||||
// Enable or disable captions based on track length
|
||||
if (this.elements) {
|
||||
toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
|
||||
}
|
||||
|
||||
// Update available language options in settings based on tracks
|
||||
update() {
|
||||
const tracks = captions.getTracks.call(this, true);
|
||||
// Get the wanted language
|
||||
const { active, language, meta, currentTrackNode } = this.captions;
|
||||
const languageExists = Boolean(tracks.find(track => track.language === language));
|
||||
// Update available languages in list
|
||||
if (
|
||||
is.array(this.config.controls) &&
|
||||
this.config.controls.includes('settings') &&
|
||||
this.config.settings.includes('captions')
|
||||
) {
|
||||
controls.setCaptionsMenu.call(this);
|
||||
}
|
||||
},
|
||||
|
||||
// Handle tracks (add event listener and "pseudo"-default)
|
||||
if (this.isHTML5 && this.isVideo) {
|
||||
tracks
|
||||
.filter(track => !meta.get(track))
|
||||
.forEach(track => {
|
||||
this.debug.log('Track added', track);
|
||||
// Attempt to store if the original dom element was "default"
|
||||
meta.set(track, {
|
||||
default: track.mode === 'showing',
|
||||
});
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Turn off native caption rendering to avoid double captions
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
track.mode = 'hidden';
|
||||
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;
|
||||
|
||||
// Add event listener for cue changes
|
||||
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
|
||||
});
|
||||
}
|
||||
// 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 });
|
||||
}
|
||||
|
||||
// Update language first time it matches, or if the previous matching track was removed
|
||||
if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
|
||||
captions.setLanguage.call(this, language);
|
||||
captions.toggle.call(this, active && languageExists);
|
||||
}
|
||||
|
||||
// Enable or disable captions based on track length
|
||||
toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
|
||||
|
||||
// Update available languages in list
|
||||
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
|
||||
controls.setCaptionsMenu.call(this);
|
||||
}
|
||||
},
|
||||
|
||||
// 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) {
|
||||
// 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);
|
||||
|
||||
// Disable captions if setting to -1
|
||||
if (index === -1) {
|
||||
captions.toggle.call(this, false, passive);
|
||||
return;
|
||||
}
|
||||
// Override user preferences to avoid switching languages if a matching track is added
|
||||
this.captions.language = track.language;
|
||||
|
||||
if (!is.number(index)) {
|
||||
this.debug.warn('Invalid caption argument', index);
|
||||
return;
|
||||
}
|
||||
// Set caption, but don't store in localStorage as user preference
|
||||
captions.set.call(this, tracks.indexOf(track));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(index in tracks)) {
|
||||
this.debug.warn('Track not found', index);
|
||||
return;
|
||||
}
|
||||
// Toggle button if it's enabled
|
||||
if (this.elements.buttons.captions) {
|
||||
this.elements.buttons.captions.pressed = active;
|
||||
}
|
||||
|
||||
if (this.captions.currentTrack !== index) {
|
||||
this.captions.currentTrack = index;
|
||||
const track = tracks[index];
|
||||
const { language } = track || {};
|
||||
// Add class hook
|
||||
toggleClass(this.elements.container, activeClass, active);
|
||||
|
||||
// Store reference to node for invalidation on remove
|
||||
this.captions.currentTrackNode = track;
|
||||
this.captions.toggled = active;
|
||||
|
||||
// Update settings menu
|
||||
controls.updateSetting.call(this, 'captions');
|
||||
// Update settings menu
|
||||
controls.updateSetting.call(this, 'captions');
|
||||
|
||||
// When passive, don't override user preferences
|
||||
if (!passive) {
|
||||
this.captions.language = language;
|
||||
this.storage.set({ language });
|
||||
}
|
||||
// Trigger event (not used internally)
|
||||
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
|
||||
}
|
||||
|
||||
// Handle Vimeo captions
|
||||
if (this.isVimeo) {
|
||||
this.embed.enableTextTrack(language);
|
||||
}
|
||||
// Wait for the call stack to clear before setting mode='hidden'
|
||||
// on the active track - forcing the browser to download it
|
||||
setTimeout(() => {
|
||||
if (active && this.captions.toggled) {
|
||||
this.captions.currentTrackNode.mode = 'hidden';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Trigger event
|
||||
triggerEvent.call(this, this.media, 'languagechange');
|
||||
}
|
||||
// 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);
|
||||
|
||||
// Show captions
|
||||
captions.toggle.call(this, true, passive);
|
||||
// Disable captions if setting to -1
|
||||
if (index === -1) {
|
||||
captions.toggle.call(this, false, passive);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isHTML5 && this.isVideo) {
|
||||
// If we change the active track while a cue is already displayed we need to update it
|
||||
captions.updateCues.call(this);
|
||||
}
|
||||
},
|
||||
if (!is.number(index)) {
|
||||
this.debug.warn('Invalid caption argument', index);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set captions by language
|
||||
// Used internally for the language setter with the passive option forced to false
|
||||
setLanguage(input, passive = true) {
|
||||
if (!is.string(input)) {
|
||||
this.debug.warn('Invalid language argument', input);
|
||||
return;
|
||||
}
|
||||
// Normalize
|
||||
const language = input.toLowerCase();
|
||||
if (!(index in tracks)) {
|
||||
this.debug.warn('Track not found', index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.captions.currentTrack !== index) {
|
||||
this.captions.currentTrack = index;
|
||||
const track = tracks[index];
|
||||
const { language } = track || {};
|
||||
|
||||
// Store reference to node for invalidation on remove
|
||||
this.captions.currentTrackNode = track;
|
||||
|
||||
// Update settings menu
|
||||
controls.updateSetting.call(this, 'captions');
|
||||
|
||||
// When passive, don't override user preferences
|
||||
if (!passive) {
|
||||
this.captions.language = language;
|
||||
this.storage.set({ language });
|
||||
}
|
||||
|
||||
// Set currentTrack
|
||||
const tracks = captions.getTracks.call(this);
|
||||
const track = captions.findTrack.call(this, [language]);
|
||||
captions.set.call(this, tracks.indexOf(track), passive);
|
||||
},
|
||||
// Handle Vimeo captions
|
||||
if (this.isVimeo) {
|
||||
this.embed.enableTextTrack(language);
|
||||
}
|
||||
|
||||
// Get current valid caption tracks
|
||||
// If update is false it will also ignore tracks without metadata
|
||||
// This is used to "freeze" the language options when captions.update is false
|
||||
getTracks(update = false) {
|
||||
// Handle media or textTracks missing or null
|
||||
const tracks = Array.from((this.media || {}).textTracks || []);
|
||||
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
|
||||
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
|
||||
return tracks
|
||||
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
|
||||
.filter(track => ['captions', 'subtitles'].includes(track.kind));
|
||||
},
|
||||
// Trigger event
|
||||
triggerEvent.call(this, this.media, 'languagechange');
|
||||
}
|
||||
|
||||
// Match tracks based on languages and get the first
|
||||
findTrack(languages, force = false) {
|
||||
const tracks = captions.getTracks.call(this);
|
||||
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
|
||||
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
|
||||
let track;
|
||||
// Show captions
|
||||
captions.toggle.call(this, true, passive);
|
||||
|
||||
languages.every(language => {
|
||||
track = sorted.find(t => t.language === language);
|
||||
return !track; // Break iteration if there is a match
|
||||
});
|
||||
if (this.isHTML5 && this.isVideo) {
|
||||
// If we change the active track while a cue is already displayed we need to update it
|
||||
captions.updateCues.call(this);
|
||||
}
|
||||
},
|
||||
|
||||
// If no match is found but is required, get first
|
||||
return track || (force ? sorted[0] : undefined);
|
||||
},
|
||||
// Set captions by language
|
||||
// Used internally for the language setter with the passive option forced to false
|
||||
setLanguage(input, passive = true) {
|
||||
if (!is.string(input)) {
|
||||
this.debug.warn('Invalid language argument', input);
|
||||
return;
|
||||
}
|
||||
// Normalize
|
||||
const language = input.toLowerCase();
|
||||
this.captions.language = language;
|
||||
|
||||
// Get the current track
|
||||
getCurrentTrack() {
|
||||
return captions.getTracks.call(this)[this.currentTrack];
|
||||
},
|
||||
// Set currentTrack
|
||||
const tracks = captions.getTracks.call(this);
|
||||
const track = captions.findTrack.call(this, [language]);
|
||||
captions.set.call(this, tracks.indexOf(track), passive);
|
||||
},
|
||||
|
||||
// Get UI label for track
|
||||
getLabel(track) {
|
||||
let currentTrack = track;
|
||||
// Get current valid caption tracks
|
||||
// If update is false it will also ignore tracks without metadata
|
||||
// This is used to "freeze" the language options when captions.update is false
|
||||
getTracks(update = false) {
|
||||
// Handle media or textTracks missing or null
|
||||
const tracks = Array.from((this.media || {}).textTracks || []);
|
||||
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
|
||||
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
|
||||
return tracks
|
||||
.filter((track) => !this.isHTML5 || update || this.captions.meta.has(track))
|
||||
.filter((track) => ['captions', 'subtitles'].includes(track.kind));
|
||||
},
|
||||
|
||||
if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
|
||||
currentTrack = captions.getCurrentTrack.call(this);
|
||||
}
|
||||
// Match tracks based on languages and get the first
|
||||
findTrack(languages, force = false) {
|
||||
const tracks = captions.getTracks.call(this);
|
||||
const sortIsDefault = (track) => Number((this.captions.meta.get(track) || {}).default);
|
||||
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
|
||||
let track;
|
||||
|
||||
if (is.track(currentTrack)) {
|
||||
if (!is.empty(currentTrack.label)) {
|
||||
return currentTrack.label;
|
||||
}
|
||||
languages.every((language) => {
|
||||
track = sorted.find((t) => t.language === language);
|
||||
return !track; // Break iteration if there is a match
|
||||
});
|
||||
|
||||
if (!is.empty(currentTrack.language)) {
|
||||
return track.language.toUpperCase();
|
||||
}
|
||||
// If no match is found but is required, get first
|
||||
return track || (force ? sorted[0] : undefined);
|
||||
},
|
||||
|
||||
return i18n.get('enabled', this.config);
|
||||
}
|
||||
// Get the current track
|
||||
getCurrentTrack() {
|
||||
return captions.getTracks.call(this)[this.currentTrack];
|
||||
},
|
||||
|
||||
return i18n.get('disabled', this.config);
|
||||
},
|
||||
// Get UI label for track
|
||||
getLabel(track) {
|
||||
let currentTrack = track;
|
||||
|
||||
// Update captions using current track's active cues
|
||||
// Also optional array argument in case there isn't any track (ex: vimeo)
|
||||
updateCues(input) {
|
||||
// Requires UI
|
||||
if (!this.supported.ui) {
|
||||
return;
|
||||
}
|
||||
if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
|
||||
currentTrack = captions.getCurrentTrack.call(this);
|
||||
}
|
||||
|
||||
if (!is.element(this.elements.captions)) {
|
||||
this.debug.warn('No captions element to render to');
|
||||
return;
|
||||
}
|
||||
if (is.track(currentTrack)) {
|
||||
if (!is.empty(currentTrack.label)) {
|
||||
return currentTrack.label;
|
||||
}
|
||||
|
||||
// Only accept array or empty input
|
||||
if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
|
||||
this.debug.warn('updateCues: Invalid input', input);
|
||||
return;
|
||||
}
|
||||
if (!is.empty(currentTrack.language)) {
|
||||
return track.language.toUpperCase();
|
||||
}
|
||||
|
||||
let cues = input;
|
||||
return i18n.get('enabled', this.config);
|
||||
}
|
||||
|
||||
// Get cues from track
|
||||
if (!cues) {
|
||||
const track = captions.getCurrentTrack.call(this);
|
||||
return i18n.get('disabled', this.config);
|
||||
},
|
||||
|
||||
cues = Array.from((track || {}).activeCues || [])
|
||||
.map(cue => cue.getCueAsHTML())
|
||||
.map(getHTML);
|
||||
}
|
||||
// Update captions using current track's active cues
|
||||
// Also optional array argument in case there isn't any track (ex: vimeo)
|
||||
updateCues(input) {
|
||||
// Requires UI
|
||||
if (!this.supported.ui) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set new caption text
|
||||
const content = cues.map(cueText => cueText.trim()).join('\n');
|
||||
const changed = content !== this.elements.captions.innerHTML;
|
||||
if (!is.element(this.elements.captions)) {
|
||||
this.debug.warn('No captions element to render to');
|
||||
return;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
// Empty the container and create a new child element
|
||||
emptyElement(this.elements.captions);
|
||||
const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
|
||||
caption.innerHTML = content;
|
||||
this.elements.captions.appendChild(caption);
|
||||
// Only accept array or empty input
|
||||
if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
|
||||
this.debug.warn('updateCues: Invalid input', input);
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger event
|
||||
triggerEvent.call(this, this.media, 'cuechange');
|
||||
}
|
||||
},
|
||||
let cues = input;
|
||||
|
||||
// Get cues from track
|
||||
if (!cues) {
|
||||
const track = captions.getCurrentTrack.call(this);
|
||||
|
||||
cues = Array.from((track || {}).activeCues || [])
|
||||
.map((cue) => cue.getCueAsHTML())
|
||||
.map(getHTML);
|
||||
}
|
||||
|
||||
// Set new caption text
|
||||
const content = cues.map((cueText) => cueText.trim()).join('\n');
|
||||
const changed = content !== this.elements.captions.innerHTML;
|
||||
|
||||
if (changed) {
|
||||
// Empty the container and create a new child element
|
||||
emptyElement(this.elements.captions);
|
||||
const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
|
||||
caption.innerHTML = content;
|
||||
this.elements.captions.appendChild(caption);
|
||||
|
||||
// Trigger event
|
||||
triggerEvent.call(this, this.media, 'cuechange');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default captions;
|
||||
|
||||
@@ -3,437 +3,459 @@
|
||||
// ==========================================================================
|
||||
|
||||
const defaults = {
|
||||
// Disable
|
||||
// Disable
|
||||
enabled: true,
|
||||
|
||||
// Custom media title
|
||||
title: '',
|
||||
|
||||
// Logging to console
|
||||
debug: false,
|
||||
|
||||
// Auto play (if supported)
|
||||
autoplay: false,
|
||||
|
||||
// Only allow one media playing at once (vimeo only)
|
||||
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
|
||||
seekTime: 10,
|
||||
|
||||
// Default volume
|
||||
volume: 1,
|
||||
muted: false,
|
||||
|
||||
// Pass a custom duration
|
||||
duration: null,
|
||||
|
||||
// Display the media duration on load in the current time position
|
||||
// If you have opted to display both duration and currentTime, this is ignored
|
||||
displayDuration: true,
|
||||
|
||||
// Invert the current time to be a countdown
|
||||
invertTime: true,
|
||||
|
||||
// Clicking the currentTime inverts it's value to show time left rather than elapsed
|
||||
toggleInvert: true,
|
||||
|
||||
// Force an aspect ratio
|
||||
// The format must be `'w:h'` (e.g. `'16:9'`)
|
||||
ratio: null,
|
||||
|
||||
// Click video container to play/pause
|
||||
clickToPlay: true,
|
||||
|
||||
// Auto hide the controls
|
||||
hideControls: true,
|
||||
|
||||
// Reset to start when playback ended
|
||||
resetOnEnd: false,
|
||||
|
||||
// Disable the standard context menu
|
||||
disableContextMenu: true,
|
||||
|
||||
// Sprite (for icons)
|
||||
loadSprite: true,
|
||||
iconPrefix: 'plyr',
|
||||
iconUrl: 'https://cdn.plyr.io/3.7.0/plyr.svg',
|
||||
|
||||
// Blank video (used to prevent errors on source change)
|
||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||
|
||||
// Quality default
|
||||
quality: {
|
||||
default: 576,
|
||||
// The options to display in the UI, if available for the source media
|
||||
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
|
||||
forced: false,
|
||||
onChange: null,
|
||||
},
|
||||
|
||||
// Set loops
|
||||
loop: {
|
||||
active: false,
|
||||
// start: null,
|
||||
// end: null,
|
||||
},
|
||||
|
||||
// Speed default and options to display
|
||||
speed: {
|
||||
selected: 1,
|
||||
// The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)
|
||||
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],
|
||||
},
|
||||
|
||||
// Keyboard shortcut settings
|
||||
keyboard: {
|
||||
focused: true,
|
||||
global: false,
|
||||
},
|
||||
|
||||
// Display tooltips
|
||||
tooltips: {
|
||||
controls: false,
|
||||
seek: true,
|
||||
},
|
||||
|
||||
// Captions settings
|
||||
captions: {
|
||||
active: false,
|
||||
language: 'auto',
|
||||
// Listen to new tracks added after Plyr is initialized.
|
||||
// This is needed for streaming captions, but may result in unselectable options
|
||||
update: false,
|
||||
},
|
||||
|
||||
// Fullscreen settings
|
||||
fullscreen: {
|
||||
enabled: true, // Allow fullscreen?
|
||||
fallback: true, // Fallback using full viewport/window
|
||||
iosNative: false, // Use the native fullscreen in iOS (disables custom controls)
|
||||
// Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode
|
||||
// Non-ancestors of the player element will be ignored
|
||||
// container: null, // defaults to the player element
|
||||
},
|
||||
|
||||
// Local storage
|
||||
storage: {
|
||||
enabled: true,
|
||||
key: 'plyr',
|
||||
},
|
||||
|
||||
// Custom media title
|
||||
title: '',
|
||||
// Default controls
|
||||
controls: [
|
||||
'play-large',
|
||||
// 'restart',
|
||||
// 'rewind',
|
||||
'play',
|
||||
// 'fast-forward',
|
||||
'progress',
|
||||
'current-time',
|
||||
// 'duration',
|
||||
'mute',
|
||||
'volume',
|
||||
'captions',
|
||||
'settings',
|
||||
'pip',
|
||||
'airplay',
|
||||
// 'download',
|
||||
'fullscreen',
|
||||
],
|
||||
settings: ['captions', 'quality', 'speed'],
|
||||
|
||||
// Logging to console
|
||||
debug: false,
|
||||
|
||||
// Auto play (if supported)
|
||||
autoplay: false,
|
||||
|
||||
// Only allow one media playing at once (vimeo only)
|
||||
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
|
||||
seekTime: 10,
|
||||
|
||||
// Default volume
|
||||
volume: 1,
|
||||
muted: false,
|
||||
|
||||
// Pass a custom duration
|
||||
duration: null,
|
||||
|
||||
// Display the media duration on load in the current time position
|
||||
// If you have opted to display both duration and currentTime, this is ignored
|
||||
displayDuration: true,
|
||||
|
||||
// Invert the current time to be a countdown
|
||||
invertTime: true,
|
||||
|
||||
// Clicking the currentTime inverts it's value to show time left rather than elapsed
|
||||
toggleInvert: true,
|
||||
|
||||
// Force an aspect ratio
|
||||
// The format must be `'w:h'` (e.g. `'16:9'`)
|
||||
ratio: null,
|
||||
|
||||
// Click video container to play/pause
|
||||
clickToPlay: true,
|
||||
|
||||
// Auto hide the controls
|
||||
hideControls: true,
|
||||
|
||||
// Reset to start when playback ended
|
||||
resetOnEnd: false,
|
||||
|
||||
// Disable the standard context menu
|
||||
disableContextMenu: true,
|
||||
|
||||
// Sprite (for icons)
|
||||
loadSprite: true,
|
||||
iconPrefix: 'plyr',
|
||||
iconUrl: 'https://cdn.plyr.io/3.5.10/plyr.svg',
|
||||
|
||||
// Blank video (used to prevent errors on source change)
|
||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||
|
||||
// Quality default
|
||||
quality: {
|
||||
default: 576,
|
||||
// The options to display in the UI, if available for the source media
|
||||
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
|
||||
forced: false,
|
||||
onChange: null,
|
||||
// Localisation
|
||||
i18n: {
|
||||
restart: 'Restart',
|
||||
rewind: 'Rewind {seektime}s',
|
||||
play: 'Play',
|
||||
pause: 'Pause',
|
||||
fastForward: 'Forward {seektime}s',
|
||||
seek: 'Seek',
|
||||
seekLabel: '{currentTime} of {duration}',
|
||||
played: 'Played',
|
||||
buffered: 'Buffered',
|
||||
currentTime: 'Current time',
|
||||
duration: 'Duration',
|
||||
volume: 'Volume',
|
||||
mute: 'Mute',
|
||||
unmute: 'Unmute',
|
||||
enableCaptions: 'Enable captions',
|
||||
disableCaptions: 'Disable captions',
|
||||
download: 'Download',
|
||||
enterFullscreen: 'Enter fullscreen',
|
||||
exitFullscreen: 'Exit fullscreen',
|
||||
frameTitle: 'Player for {title}',
|
||||
captions: 'Captions',
|
||||
settings: 'Settings',
|
||||
pip: 'PIP',
|
||||
menuBack: 'Go back to previous menu',
|
||||
speed: 'Speed',
|
||||
normal: 'Normal',
|
||||
quality: 'Quality',
|
||||
loop: 'Loop',
|
||||
start: 'Start',
|
||||
end: 'End',
|
||||
all: 'All',
|
||||
reset: 'Reset',
|
||||
disabled: 'Disabled',
|
||||
enabled: 'Enabled',
|
||||
advertisement: 'Ad',
|
||||
qualityBadge: {
|
||||
2160: '4K',
|
||||
1440: 'HD',
|
||||
1080: 'HD',
|
||||
720: 'HD',
|
||||
576: 'SD',
|
||||
480: 'SD',
|
||||
},
|
||||
},
|
||||
|
||||
// Set loops
|
||||
loop: {
|
||||
active: false,
|
||||
// start: null,
|
||||
// end: null,
|
||||
},
|
||||
|
||||
// Speed default and options to display
|
||||
speed: {
|
||||
selected: 1,
|
||||
// The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)
|
||||
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],
|
||||
},
|
||||
|
||||
// Keyboard shortcut settings
|
||||
keyboard: {
|
||||
focused: true,
|
||||
global: false,
|
||||
},
|
||||
|
||||
// Display tooltips
|
||||
tooltips: {
|
||||
controls: false,
|
||||
seek: true,
|
||||
},
|
||||
|
||||
// Captions settings
|
||||
captions: {
|
||||
active: false,
|
||||
language: 'auto',
|
||||
// Listen to new tracks added after Plyr is initialized.
|
||||
// This is needed for streaming captions, but may result in unselectable options
|
||||
update: false,
|
||||
},
|
||||
|
||||
// Fullscreen settings
|
||||
fullscreen: {
|
||||
enabled: true, // Allow fullscreen?
|
||||
fallback: true, // Fallback using full viewport/window
|
||||
iosNative: false, // Use the native fullscreen in iOS (disables custom controls)
|
||||
},
|
||||
|
||||
// Local storage
|
||||
storage: {
|
||||
enabled: true,
|
||||
key: 'plyr',
|
||||
},
|
||||
|
||||
// Default controls
|
||||
controls: [
|
||||
'play-large',
|
||||
// 'restart',
|
||||
// 'rewind',
|
||||
'play',
|
||||
// 'fast-forward',
|
||||
'progress',
|
||||
'current-time',
|
||||
// 'duration',
|
||||
'mute',
|
||||
'volume',
|
||||
'captions',
|
||||
'settings',
|
||||
'pip',
|
||||
'airplay',
|
||||
// 'download',
|
||||
'fullscreen',
|
||||
],
|
||||
settings: ['captions', 'quality', 'speed'],
|
||||
|
||||
// Localisation
|
||||
i18n: {
|
||||
restart: 'Restart',
|
||||
rewind: 'Rewind {seektime}s',
|
||||
play: 'Play',
|
||||
pause: 'Pause',
|
||||
fastForward: 'Forward {seektime}s',
|
||||
seek: 'Seek',
|
||||
seekLabel: '{currentTime} of {duration}',
|
||||
played: 'Played',
|
||||
buffered: 'Buffered',
|
||||
currentTime: 'Current time',
|
||||
duration: 'Duration',
|
||||
volume: 'Volume',
|
||||
mute: 'Mute',
|
||||
unmute: 'Unmute',
|
||||
enableCaptions: 'Enable captions',
|
||||
disableCaptions: 'Disable captions',
|
||||
download: 'Download',
|
||||
enterFullscreen: 'Enter fullscreen',
|
||||
exitFullscreen: 'Exit fullscreen',
|
||||
frameTitle: 'Player for {title}',
|
||||
captions: 'Captions',
|
||||
settings: 'Settings',
|
||||
pip: 'PIP',
|
||||
menuBack: 'Go back to previous menu',
|
||||
speed: 'Speed',
|
||||
normal: 'Normal',
|
||||
quality: 'Quality',
|
||||
loop: 'Loop',
|
||||
start: 'Start',
|
||||
end: 'End',
|
||||
all: 'All',
|
||||
reset: 'Reset',
|
||||
disabled: 'Disabled',
|
||||
enabled: 'Enabled',
|
||||
advertisement: 'Ad',
|
||||
qualityBadge: {
|
||||
2160: '4K',
|
||||
1440: 'HD',
|
||||
1080: 'HD',
|
||||
720: 'HD',
|
||||
576: 'SD',
|
||||
480: 'SD',
|
||||
},
|
||||
},
|
||||
|
||||
// URLs
|
||||
urls: {
|
||||
download: null,
|
||||
vimeo: {
|
||||
sdk: 'https://player.vimeo.com/api/player.js',
|
||||
iframe: 'https://player.vimeo.com/video/{0}?{1}',
|
||||
api: 'https://vimeo.com/api/v2/video/{0}.json',
|
||||
},
|
||||
youtube: {
|
||||
sdk: 'https://www.youtube.com/iframe_api',
|
||||
api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',
|
||||
},
|
||||
googleIMA: {
|
||||
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
||||
},
|
||||
},
|
||||
|
||||
// Custom control listeners
|
||||
listeners: {
|
||||
seek: null,
|
||||
play: null,
|
||||
pause: null,
|
||||
restart: null,
|
||||
rewind: null,
|
||||
fastForward: null,
|
||||
mute: null,
|
||||
volume: null,
|
||||
captions: null,
|
||||
download: null,
|
||||
fullscreen: null,
|
||||
pip: null,
|
||||
airplay: null,
|
||||
speed: null,
|
||||
quality: null,
|
||||
loop: null,
|
||||
language: null,
|
||||
},
|
||||
|
||||
// Events to watch and bubble
|
||||
events: [
|
||||
// Events to watch on HTML5 media elements and bubble
|
||||
// https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
|
||||
'ended',
|
||||
'progress',
|
||||
'stalled',
|
||||
'playing',
|
||||
'waiting',
|
||||
'canplay',
|
||||
'canplaythrough',
|
||||
'loadstart',
|
||||
'loadeddata',
|
||||
'loadedmetadata',
|
||||
'timeupdate',
|
||||
'volumechange',
|
||||
'play',
|
||||
'pause',
|
||||
'error',
|
||||
'seeking',
|
||||
'seeked',
|
||||
'emptied',
|
||||
'ratechange',
|
||||
'cuechange',
|
||||
|
||||
// Custom events
|
||||
'download',
|
||||
'enterfullscreen',
|
||||
'exitfullscreen',
|
||||
'captionsenabled',
|
||||
'captionsdisabled',
|
||||
'languagechange',
|
||||
'controlshidden',
|
||||
'controlsshown',
|
||||
'ready',
|
||||
|
||||
// YouTube
|
||||
'statechange',
|
||||
|
||||
// Quality
|
||||
'qualitychange',
|
||||
|
||||
// Ads
|
||||
'adsloaded',
|
||||
'adscontentpause',
|
||||
'adscontentresume',
|
||||
'adstarted',
|
||||
'adsmidpoint',
|
||||
'adscomplete',
|
||||
'adsallcomplete',
|
||||
'adsimpression',
|
||||
'adsclick',
|
||||
],
|
||||
|
||||
// Selectors
|
||||
// Change these to match your template if using custom HTML
|
||||
selectors: {
|
||||
editable: 'input, textarea, select, [contenteditable]',
|
||||
container: '.plyr',
|
||||
controls: {
|
||||
container: null,
|
||||
wrapper: '.plyr__controls',
|
||||
},
|
||||
labels: '[data-plyr]',
|
||||
buttons: {
|
||||
play: '[data-plyr="play"]',
|
||||
pause: '[data-plyr="pause"]',
|
||||
restart: '[data-plyr="restart"]',
|
||||
rewind: '[data-plyr="rewind"]',
|
||||
fastForward: '[data-plyr="fast-forward"]',
|
||||
mute: '[data-plyr="mute"]',
|
||||
captions: '[data-plyr="captions"]',
|
||||
download: '[data-plyr="download"]',
|
||||
fullscreen: '[data-plyr="fullscreen"]',
|
||||
pip: '[data-plyr="pip"]',
|
||||
airplay: '[data-plyr="airplay"]',
|
||||
settings: '[data-plyr="settings"]',
|
||||
loop: '[data-plyr="loop"]',
|
||||
},
|
||||
inputs: {
|
||||
seek: '[data-plyr="seek"]',
|
||||
volume: '[data-plyr="volume"]',
|
||||
speed: '[data-plyr="speed"]',
|
||||
language: '[data-plyr="language"]',
|
||||
quality: '[data-plyr="quality"]',
|
||||
},
|
||||
display: {
|
||||
currentTime: '.plyr__time--current',
|
||||
duration: '.plyr__time--duration',
|
||||
buffer: '.plyr__progress__buffer',
|
||||
loop: '.plyr__progress__loop', // Used later
|
||||
volume: '.plyr__volume--display',
|
||||
},
|
||||
progress: '.plyr__progress',
|
||||
captions: '.plyr__captions',
|
||||
caption: '.plyr__caption',
|
||||
},
|
||||
|
||||
// Class hooks added to the player in different states
|
||||
classNames: {
|
||||
type: 'plyr--{0}',
|
||||
provider: 'plyr--{0}',
|
||||
video: 'plyr__video-wrapper',
|
||||
embed: 'plyr__video-embed',
|
||||
videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',
|
||||
embedContainer: 'plyr__video-embed__container',
|
||||
poster: 'plyr__poster',
|
||||
posterEnabled: 'plyr__poster-enabled',
|
||||
ads: 'plyr__ads',
|
||||
control: 'plyr__control',
|
||||
controlPressed: 'plyr__control--pressed',
|
||||
playing: 'plyr--playing',
|
||||
paused: 'plyr--paused',
|
||||
stopped: 'plyr--stopped',
|
||||
loading: 'plyr--loading',
|
||||
hover: 'plyr--hover',
|
||||
tooltip: 'plyr__tooltip',
|
||||
cues: 'plyr__cues',
|
||||
hidden: 'plyr__sr-only',
|
||||
hideControls: 'plyr--hide-controls',
|
||||
isIos: 'plyr--is-ios',
|
||||
isTouch: 'plyr--is-touch',
|
||||
uiSupported: 'plyr--full-ui',
|
||||
noTransition: 'plyr--no-transition',
|
||||
display: {
|
||||
time: 'plyr__time',
|
||||
},
|
||||
menu: {
|
||||
value: 'plyr__menu__value',
|
||||
badge: 'plyr__badge',
|
||||
open: 'plyr--menu-open',
|
||||
},
|
||||
captions: {
|
||||
enabled: 'plyr--captions-enabled',
|
||||
active: 'plyr--captions-active',
|
||||
},
|
||||
fullscreen: {
|
||||
enabled: 'plyr--fullscreen-enabled',
|
||||
fallback: 'plyr--fullscreen-fallback',
|
||||
},
|
||||
pip: {
|
||||
supported: 'plyr--pip-supported',
|
||||
active: 'plyr--pip-active',
|
||||
},
|
||||
airplay: {
|
||||
supported: 'plyr--airplay-supported',
|
||||
active: 'plyr--airplay-active',
|
||||
},
|
||||
tabFocus: 'plyr__tab-focus',
|
||||
previewThumbnails: {
|
||||
// Tooltip thumbs
|
||||
thumbContainer: 'plyr__preview-thumb',
|
||||
thumbContainerShown: 'plyr__preview-thumb--is-shown',
|
||||
imageContainer: 'plyr__preview-thumb__image-container',
|
||||
timeContainer: 'plyr__preview-thumb__time-container',
|
||||
// Scrubbing
|
||||
scrubbingContainer: 'plyr__preview-scrubbing',
|
||||
scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',
|
||||
},
|
||||
},
|
||||
|
||||
// Embed attributes
|
||||
attributes: {
|
||||
embed: {
|
||||
provider: 'data-plyr-provider',
|
||||
id: 'data-plyr-embed-id',
|
||||
},
|
||||
},
|
||||
|
||||
// Advertisements plugin
|
||||
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
|
||||
ads: {
|
||||
enabled: false,
|
||||
publisherId: '',
|
||||
tagUrl: '',
|
||||
},
|
||||
|
||||
// Preview Thumbnails plugin
|
||||
previewThumbnails: {
|
||||
enabled: false,
|
||||
src: '',
|
||||
},
|
||||
|
||||
// Vimeo plugin
|
||||
// URLs
|
||||
urls: {
|
||||
download: null,
|
||||
vimeo: {
|
||||
byline: false,
|
||||
portrait: false,
|
||||
title: false,
|
||||
speed: true,
|
||||
transparent: false,
|
||||
// These settings require a pro or premium account to work
|
||||
sidedock: false,
|
||||
controls: false,
|
||||
// Custom settings from Plyr
|
||||
referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
|
||||
sdk: 'https://player.vimeo.com/api/player.js',
|
||||
iframe: 'https://player.vimeo.com/video/{0}?{1}',
|
||||
api: 'https://vimeo.com/api/oembed.json?url={0}',
|
||||
},
|
||||
|
||||
// YouTube plugin
|
||||
youtube: {
|
||||
noCookie: false, // Whether to use an alternative version of YouTube without cookies
|
||||
rel: 0, // No related vids
|
||||
showinfo: 0, // Hide info
|
||||
iv_load_policy: 3, // Hide annotations
|
||||
modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)
|
||||
sdk: 'https://www.youtube.com/iframe_api',
|
||||
api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',
|
||||
},
|
||||
googleIMA: {
|
||||
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
||||
},
|
||||
},
|
||||
|
||||
// Custom control listeners
|
||||
listeners: {
|
||||
seek: null,
|
||||
play: null,
|
||||
pause: null,
|
||||
restart: null,
|
||||
rewind: null,
|
||||
fastForward: null,
|
||||
mute: null,
|
||||
volume: null,
|
||||
captions: null,
|
||||
download: null,
|
||||
fullscreen: null,
|
||||
pip: null,
|
||||
airplay: null,
|
||||
speed: null,
|
||||
quality: null,
|
||||
loop: null,
|
||||
language: null,
|
||||
},
|
||||
|
||||
// Events to watch and bubble
|
||||
events: [
|
||||
// Events to watch on HTML5 media elements and bubble
|
||||
// https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
|
||||
'ended',
|
||||
'progress',
|
||||
'stalled',
|
||||
'playing',
|
||||
'waiting',
|
||||
'canplay',
|
||||
'canplaythrough',
|
||||
'loadstart',
|
||||
'loadeddata',
|
||||
'loadedmetadata',
|
||||
'timeupdate',
|
||||
'volumechange',
|
||||
'play',
|
||||
'pause',
|
||||
'error',
|
||||
'seeking',
|
||||
'seeked',
|
||||
'emptied',
|
||||
'ratechange',
|
||||
'cuechange',
|
||||
|
||||
// Custom events
|
||||
'download',
|
||||
'enterfullscreen',
|
||||
'exitfullscreen',
|
||||
'captionsenabled',
|
||||
'captionsdisabled',
|
||||
'languagechange',
|
||||
'controlshidden',
|
||||
'controlsshown',
|
||||
'ready',
|
||||
|
||||
// YouTube
|
||||
'statechange',
|
||||
|
||||
// Quality
|
||||
'qualitychange',
|
||||
|
||||
// Ads
|
||||
'adsloaded',
|
||||
'adscontentpause',
|
||||
'adscontentresume',
|
||||
'adstarted',
|
||||
'adsmidpoint',
|
||||
'adscomplete',
|
||||
'adsallcomplete',
|
||||
'adsimpression',
|
||||
'adsclick',
|
||||
],
|
||||
|
||||
// Selectors
|
||||
// Change these to match your template if using custom HTML
|
||||
selectors: {
|
||||
editable: 'input, textarea, select, [contenteditable]',
|
||||
container: '.plyr',
|
||||
controls: {
|
||||
container: null,
|
||||
wrapper: '.plyr__controls',
|
||||
},
|
||||
labels: '[data-plyr]',
|
||||
buttons: {
|
||||
play: '[data-plyr="play"]',
|
||||
pause: '[data-plyr="pause"]',
|
||||
restart: '[data-plyr="restart"]',
|
||||
rewind: '[data-plyr="rewind"]',
|
||||
fastForward: '[data-plyr="fast-forward"]',
|
||||
mute: '[data-plyr="mute"]',
|
||||
captions: '[data-plyr="captions"]',
|
||||
download: '[data-plyr="download"]',
|
||||
fullscreen: '[data-plyr="fullscreen"]',
|
||||
pip: '[data-plyr="pip"]',
|
||||
airplay: '[data-plyr="airplay"]',
|
||||
settings: '[data-plyr="settings"]',
|
||||
loop: '[data-plyr="loop"]',
|
||||
},
|
||||
inputs: {
|
||||
seek: '[data-plyr="seek"]',
|
||||
volume: '[data-plyr="volume"]',
|
||||
speed: '[data-plyr="speed"]',
|
||||
language: '[data-plyr="language"]',
|
||||
quality: '[data-plyr="quality"]',
|
||||
},
|
||||
display: {
|
||||
currentTime: '.plyr__time--current',
|
||||
duration: '.plyr__time--duration',
|
||||
buffer: '.plyr__progress__buffer',
|
||||
loop: '.plyr__progress__loop', // Used later
|
||||
volume: '.plyr__volume--display',
|
||||
},
|
||||
progress: '.plyr__progress',
|
||||
captions: '.plyr__captions',
|
||||
caption: '.plyr__caption',
|
||||
},
|
||||
|
||||
// Class hooks added to the player in different states
|
||||
classNames: {
|
||||
type: 'plyr--{0}',
|
||||
provider: 'plyr--{0}',
|
||||
video: 'plyr__video-wrapper',
|
||||
embed: 'plyr__video-embed',
|
||||
videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',
|
||||
embedContainer: 'plyr__video-embed__container',
|
||||
poster: 'plyr__poster',
|
||||
posterEnabled: 'plyr__poster-enabled',
|
||||
ads: 'plyr__ads',
|
||||
control: 'plyr__control',
|
||||
controlPressed: 'plyr__control--pressed',
|
||||
playing: 'plyr--playing',
|
||||
paused: 'plyr--paused',
|
||||
stopped: 'plyr--stopped',
|
||||
loading: 'plyr--loading',
|
||||
hover: 'plyr--hover',
|
||||
tooltip: 'plyr__tooltip',
|
||||
cues: 'plyr__cues',
|
||||
marker: 'plyr__progress__marker',
|
||||
hidden: 'plyr__sr-only',
|
||||
hideControls: 'plyr--hide-controls',
|
||||
isIos: 'plyr--is-ios',
|
||||
isTouch: 'plyr--is-touch',
|
||||
uiSupported: 'plyr--full-ui',
|
||||
noTransition: 'plyr--no-transition',
|
||||
display: {
|
||||
time: 'plyr__time',
|
||||
},
|
||||
menu: {
|
||||
value: 'plyr__menu__value',
|
||||
badge: 'plyr__badge',
|
||||
open: 'plyr--menu-open',
|
||||
},
|
||||
captions: {
|
||||
enabled: 'plyr--captions-enabled',
|
||||
active: 'plyr--captions-active',
|
||||
},
|
||||
fullscreen: {
|
||||
enabled: 'plyr--fullscreen-enabled',
|
||||
fallback: 'plyr--fullscreen-fallback',
|
||||
},
|
||||
pip: {
|
||||
supported: 'plyr--pip-supported',
|
||||
active: 'plyr--pip-active',
|
||||
},
|
||||
airplay: {
|
||||
supported: 'plyr--airplay-supported',
|
||||
active: 'plyr--airplay-active',
|
||||
},
|
||||
tabFocus: 'plyr__tab-focus',
|
||||
previewThumbnails: {
|
||||
// Tooltip thumbs
|
||||
thumbContainer: 'plyr__preview-thumb',
|
||||
thumbContainerShown: 'plyr__preview-thumb--is-shown',
|
||||
imageContainer: 'plyr__preview-thumb__image-container',
|
||||
timeContainer: 'plyr__preview-thumb__time-container',
|
||||
// Scrubbing
|
||||
scrubbingContainer: 'plyr__preview-scrubbing',
|
||||
scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',
|
||||
},
|
||||
},
|
||||
|
||||
// Embed attributes
|
||||
attributes: {
|
||||
embed: {
|
||||
provider: 'data-plyr-provider',
|
||||
id: 'data-plyr-embed-id',
|
||||
hash: 'data-plyr-embed-hash',
|
||||
},
|
||||
},
|
||||
|
||||
// Advertisements plugin
|
||||
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
|
||||
ads: {
|
||||
enabled: false,
|
||||
publisherId: '',
|
||||
tagUrl: '',
|
||||
},
|
||||
|
||||
// Preview Thumbnails plugin
|
||||
previewThumbnails: {
|
||||
enabled: false,
|
||||
src: '',
|
||||
},
|
||||
|
||||
// Vimeo plugin
|
||||
vimeo: {
|
||||
byline: false,
|
||||
portrait: false,
|
||||
title: false,
|
||||
speed: true,
|
||||
transparent: false,
|
||||
// Custom settings from Plyr
|
||||
customControls: true,
|
||||
referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
|
||||
// Whether the owner of the video has a Pro or Business account
|
||||
// (which allows us to properly hide controls without CSS hacks, etc)
|
||||
premium: false,
|
||||
},
|
||||
|
||||
// YouTube plugin
|
||||
youtube: {
|
||||
rel: 0, // No related vids
|
||||
showinfo: 0, // Hide info
|
||||
iv_load_policy: 3, // Hide annotations
|
||||
modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)
|
||||
// Custom settings from Plyr
|
||||
customControls: true,
|
||||
noCookie: false, // Whether to use an alternative version of YouTube without cookies
|
||||
},
|
||||
|
||||
// Media Metadata
|
||||
mediaMetadata: {
|
||||
title: '',
|
||||
artist: '',
|
||||
album: '',
|
||||
artwork: [],
|
||||
},
|
||||
|
||||
// Markers
|
||||
markers: {
|
||||
enabled: false,
|
||||
points: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default defaults;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// ==========================================================================
|
||||
|
||||
export const pip = {
|
||||
active: 'picture-in-picture',
|
||||
inactive: 'inline',
|
||||
active: 'picture-in-picture',
|
||||
inactive: 'inline',
|
||||
};
|
||||
|
||||
export default { pip };
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
// ==========================================================================
|
||||
|
||||
export const providers = {
|
||||
html5: 'html5',
|
||||
youtube: 'youtube',
|
||||
vimeo: 'vimeo',
|
||||
html5: 'html5',
|
||||
youtube: 'youtube',
|
||||
vimeo: 'vimeo',
|
||||
};
|
||||
|
||||
export const types = {
|
||||
audio: 'audio',
|
||||
video: 'video',
|
||||
audio: 'audio',
|
||||
video: 'video',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -18,17 +18,17 @@ export const types = {
|
||||
* @param {String} url
|
||||
*/
|
||||
export function getProviderByUrl(url) {
|
||||
// YouTube
|
||||
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) {
|
||||
return providers.youtube;
|
||||
}
|
||||
// YouTube
|
||||
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) {
|
||||
return providers.youtube;
|
||||
}
|
||||
|
||||
// Vimeo
|
||||
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
|
||||
return providers.vimeo;
|
||||
}
|
||||
// Vimeo
|
||||
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
|
||||
return providers.vimeo;
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
export default { providers, types };
|
||||
|
||||
@@ -5,26 +5,26 @@
|
||||
const noop = () => {};
|
||||
|
||||
export default class Console {
|
||||
constructor(enabled = false) {
|
||||
this.enabled = window.console && enabled;
|
||||
constructor(enabled = false) {
|
||||
this.enabled = window.console && enabled;
|
||||
|
||||
if (this.enabled) {
|
||||
this.log('Debugging enabled');
|
||||
}
|
||||
if (this.enabled) {
|
||||
this.log('Debugging enabled');
|
||||
}
|
||||
}
|
||||
|
||||
get log() {
|
||||
// eslint-disable-next-line no-console
|
||||
return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
|
||||
}
|
||||
get log() {
|
||||
// eslint-disable-next-line no-console
|
||||
return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
|
||||
}
|
||||
|
||||
get warn() {
|
||||
// eslint-disable-next-line no-console
|
||||
return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
|
||||
}
|
||||
get warn() {
|
||||
// eslint-disable-next-line no-console
|
||||
return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
|
||||
}
|
||||
|
||||
get error() {
|
||||
// eslint-disable-next-line no-console
|
||||
return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
|
||||
}
|
||||
get error() {
|
||||
// eslint-disable-next-line no-console
|
||||
return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
|
||||
}
|
||||
}
|
||||
|
||||
3266
src/js/controls.js
vendored
3266
src/js/controls.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -5,288 +5,301 @@
|
||||
// ==========================================================================
|
||||
|
||||
import browser from './utils/browser';
|
||||
import { getElements, hasClass, toggleClass } from './utils/elements';
|
||||
import { closest, getElements, hasClass, toggleClass } from './utils/elements';
|
||||
import { on, triggerEvent } from './utils/events';
|
||||
import is from './utils/is';
|
||||
import { silencePromise } from './utils/promise';
|
||||
|
||||
class Fullscreen {
|
||||
constructor(player) {
|
||||
// Keep reference to parent
|
||||
this.player = player;
|
||||
constructor(player) {
|
||||
// Keep reference to parent
|
||||
this.player = player;
|
||||
|
||||
// Get prefix
|
||||
this.prefix = Fullscreen.prefix;
|
||||
this.property = Fullscreen.property;
|
||||
// Get prefix
|
||||
this.prefix = Fullscreen.prefix;
|
||||
this.property = Fullscreen.property;
|
||||
|
||||
// Scroll position
|
||||
this.scrollPosition = { x: 0, y: 0 };
|
||||
// Scroll position
|
||||
this.scrollPosition = { x: 0, y: 0 };
|
||||
|
||||
// Force the use of 'full window/browser' rather than fullscreen
|
||||
this.forceFallback = player.config.fullscreen.fallback === 'force';
|
||||
// Force the use of 'full window/browser' rather than fullscreen
|
||||
this.forceFallback = player.config.fullscreen.fallback === 'force';
|
||||
|
||||
// Register event listeners
|
||||
// Handle event (incase user presses escape etc)
|
||||
on.call(
|
||||
this.player,
|
||||
document,
|
||||
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
|
||||
() => {
|
||||
// TODO: Filter for target??
|
||||
this.onChange();
|
||||
},
|
||||
);
|
||||
// Get the fullscreen element
|
||||
// Checks container is an ancestor, defaults to null
|
||||
this.player.elements.fullscreen =
|
||||
player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container);
|
||||
|
||||
// Fullscreen toggle on double click
|
||||
on.call(this.player, this.player.elements.container, 'dblclick', event => {
|
||||
// Ignore double click in controls
|
||||
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.toggle();
|
||||
});
|
||||
|
||||
// Tap focus when in fullscreen
|
||||
on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));
|
||||
|
||||
// Update the UI
|
||||
this.update();
|
||||
}
|
||||
|
||||
// Determine if native supported
|
||||
static get native() {
|
||||
return !!(
|
||||
document.fullscreenEnabled ||
|
||||
document.webkitFullscreenEnabled ||
|
||||
document.mozFullScreenEnabled ||
|
||||
document.msFullscreenEnabled
|
||||
);
|
||||
}
|
||||
|
||||
// If we're actually using native
|
||||
get usingNative() {
|
||||
return Fullscreen.native && !this.forceFallback;
|
||||
}
|
||||
|
||||
// Get the prefix for handlers
|
||||
static get prefix() {
|
||||
// No prefix
|
||||
if (is.function(document.exitFullscreen)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Check for fullscreen support by vendor prefix
|
||||
let value = '';
|
||||
const prefixes = ['webkit', 'moz', 'ms'];
|
||||
|
||||
prefixes.some(pre => {
|
||||
if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
|
||||
value = pre;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
static get property() {
|
||||
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
|
||||
}
|
||||
|
||||
// Determine if fullscreen is enabled
|
||||
get enabled() {
|
||||
return (
|
||||
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
|
||||
this.player.config.fullscreen.enabled &&
|
||||
this.player.supported.ui &&
|
||||
this.player.isVideo
|
||||
);
|
||||
}
|
||||
|
||||
// Get active state
|
||||
get active() {
|
||||
if (!this.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fallback using classname
|
||||
if (!Fullscreen.native || this.forceFallback) {
|
||||
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
|
||||
}
|
||||
|
||||
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
|
||||
|
||||
return element === this.target;
|
||||
}
|
||||
|
||||
// Get target element
|
||||
get target() {
|
||||
return browser.isIos && this.player.config.fullscreen.iosNative
|
||||
? this.player.media
|
||||
: this.player.elements.container;
|
||||
}
|
||||
|
||||
onChange() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update toggle button
|
||||
const button = this.player.elements.buttons.fullscreen;
|
||||
if (is.element(button)) {
|
||||
button.pressed = this.active;
|
||||
}
|
||||
|
||||
// Trigger an event
|
||||
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
|
||||
}
|
||||
|
||||
toggleFallback(toggle = false) {
|
||||
// Store or restore scroll position
|
||||
if (toggle) {
|
||||
this.scrollPosition = {
|
||||
x: window.scrollX || 0,
|
||||
y: window.scrollY || 0,
|
||||
};
|
||||
} else {
|
||||
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
|
||||
}
|
||||
|
||||
// Toggle scroll
|
||||
document.body.style.overflow = toggle ? 'hidden' : '';
|
||||
|
||||
// Toggle class hook
|
||||
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
|
||||
|
||||
// Force full viewport on iPhone X+
|
||||
if (browser.isIos) {
|
||||
let viewport = document.head.querySelector('meta[name="viewport"]');
|
||||
const property = 'viewport-fit=cover';
|
||||
|
||||
// Inject the viewport meta if required
|
||||
if (!viewport) {
|
||||
viewport = document.createElement('meta');
|
||||
viewport.setAttribute('name', 'viewport');
|
||||
}
|
||||
|
||||
// Check if the property already exists
|
||||
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
|
||||
|
||||
if (toggle) {
|
||||
this.cleanupViewport = !hasProperty;
|
||||
|
||||
if (!hasProperty) {
|
||||
viewport.content += `,${property}`;
|
||||
}
|
||||
} else if (this.cleanupViewport) {
|
||||
viewport.content = viewport.content
|
||||
.split(',')
|
||||
.filter(part => part.trim() !== property)
|
||||
.join(',');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle button and fire events
|
||||
// Register event listeners
|
||||
// Handle event (incase user presses escape etc)
|
||||
on.call(
|
||||
this.player,
|
||||
document,
|
||||
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
|
||||
() => {
|
||||
// TODO: Filter for target??
|
||||
this.onChange();
|
||||
},
|
||||
);
|
||||
|
||||
// Fullscreen toggle on double click
|
||||
on.call(this.player, this.player.elements.container, 'dblclick', (event) => {
|
||||
// Ignore double click in controls
|
||||
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.player.listeners.proxy(event, this.toggle, 'fullscreen');
|
||||
});
|
||||
|
||||
// Tap focus when in fullscreen
|
||||
on.call(this, this.player.elements.container, 'keydown', (event) => this.trapFocus(event));
|
||||
|
||||
// Update the UI
|
||||
this.update();
|
||||
|
||||
// this.toggle = this.toggle.bind(this);
|
||||
}
|
||||
|
||||
// Determine if native supported
|
||||
static get native() {
|
||||
return !!(
|
||||
document.fullscreenEnabled ||
|
||||
document.webkitFullscreenEnabled ||
|
||||
document.mozFullScreenEnabled ||
|
||||
document.msFullscreenEnabled
|
||||
);
|
||||
}
|
||||
|
||||
// If we're actually using native
|
||||
get usingNative() {
|
||||
return Fullscreen.native && !this.forceFallback;
|
||||
}
|
||||
|
||||
// Get the prefix for handlers
|
||||
static get prefix() {
|
||||
// No prefix
|
||||
if (is.function(document.exitFullscreen)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Trap focus inside container
|
||||
trapFocus(event) {
|
||||
// Bail if iOS, not active, not the tab key
|
||||
if (browser.isIos || !this.active || event.key !== 'Tab' || event.keyCode !== 9) {
|
||||
return;
|
||||
}
|
||||
// Check for fullscreen support by vendor prefix
|
||||
let value = '';
|
||||
const prefixes = ['webkit', 'moz', 'ms'];
|
||||
|
||||
// Get the current focused element
|
||||
const focused = document.activeElement;
|
||||
const focusable = getElements.call(
|
||||
this.player,
|
||||
'a[href], button:not(:disabled), input:not(:disabled), [tabindex]',
|
||||
);
|
||||
const [first] = focusable;
|
||||
const last = focusable[focusable.length - 1];
|
||||
prefixes.some((pre) => {
|
||||
if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
|
||||
value = pre;
|
||||
return true;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
static get property() {
|
||||
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
|
||||
}
|
||||
|
||||
// Determine if fullscreen is enabled
|
||||
get enabled() {
|
||||
return (
|
||||
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
|
||||
this.player.config.fullscreen.enabled &&
|
||||
this.player.supported.ui &&
|
||||
this.player.isVideo
|
||||
);
|
||||
}
|
||||
|
||||
// Get active state
|
||||
get active() {
|
||||
if (!this.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update UI
|
||||
update() {
|
||||
if (this.enabled) {
|
||||
let mode;
|
||||
|
||||
if (this.forceFallback) {
|
||||
mode = 'Fallback (forced)';
|
||||
} else if (Fullscreen.native) {
|
||||
mode = 'Native';
|
||||
} else {
|
||||
mode = 'Fallback';
|
||||
}
|
||||
|
||||
this.player.debug.log(`${mode} fullscreen enabled`);
|
||||
} else {
|
||||
this.player.debug.log('Fullscreen not supported and fallback disabled');
|
||||
}
|
||||
|
||||
// Add styling hook to show button
|
||||
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
|
||||
// Fallback using classname
|
||||
if (!Fullscreen.native || this.forceFallback) {
|
||||
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
|
||||
}
|
||||
|
||||
// Make an element fullscreen
|
||||
enter() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
const element = !this.prefix
|
||||
? this.target.getRootNode().fullscreenElement
|
||||
: this.target.getRootNode()[`${this.prefix}${this.property}Element`];
|
||||
|
||||
// iOS native fullscreen doesn't need the request step
|
||||
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
||||
this.target.webkitEnterFullscreen();
|
||||
} else if (!Fullscreen.native || this.forceFallback) {
|
||||
this.toggleFallback(true);
|
||||
} else if (!this.prefix) {
|
||||
this.target.requestFullscreen({ navigationUI: 'hide' });
|
||||
} else if (!is.empty(this.prefix)) {
|
||||
this.target[`${this.prefix}Request${this.property}`]();
|
||||
}
|
||||
return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;
|
||||
}
|
||||
|
||||
// Get target element
|
||||
get target() {
|
||||
return browser.isIos && this.player.config.fullscreen.iosNative
|
||||
? this.player.media
|
||||
: this.player.elements.fullscreen || this.player.elements.container;
|
||||
}
|
||||
|
||||
onChange = () => {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bail from fullscreen
|
||||
exit() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// iOS native fullscreen
|
||||
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
||||
this.target.webkitExitFullscreen();
|
||||
this.player.play();
|
||||
} else if (!Fullscreen.native || this.forceFallback) {
|
||||
this.toggleFallback(false);
|
||||
} else if (!this.prefix) {
|
||||
(document.cancelFullScreen || document.exitFullscreen).call(document);
|
||||
} else if (!is.empty(this.prefix)) {
|
||||
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
|
||||
document[`${this.prefix}${action}${this.property}`]();
|
||||
}
|
||||
// Update toggle button
|
||||
const button = this.player.elements.buttons.fullscreen;
|
||||
if (is.element(button)) {
|
||||
button.pressed = this.active;
|
||||
}
|
||||
|
||||
// Toggle state
|
||||
toggle() {
|
||||
if (!this.active) {
|
||||
this.enter();
|
||||
} else {
|
||||
this.exit();
|
||||
}
|
||||
// Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up
|
||||
const target = this.target === this.player.media ? this.target : this.player.elements.container;
|
||||
// Trigger an event
|
||||
triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
|
||||
};
|
||||
|
||||
toggleFallback = (toggle = false) => {
|
||||
// Store or restore scroll position
|
||||
if (toggle) {
|
||||
this.scrollPosition = {
|
||||
x: window.scrollX || 0,
|
||||
y: window.scrollY || 0,
|
||||
};
|
||||
} else {
|
||||
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
|
||||
}
|
||||
|
||||
// Toggle scroll
|
||||
document.body.style.overflow = toggle ? 'hidden' : '';
|
||||
|
||||
// Toggle class hook
|
||||
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
|
||||
|
||||
// Force full viewport on iPhone X+
|
||||
if (browser.isIos) {
|
||||
let viewport = document.head.querySelector('meta[name="viewport"]');
|
||||
const property = 'viewport-fit=cover';
|
||||
|
||||
// Inject the viewport meta if required
|
||||
if (!viewport) {
|
||||
viewport = document.createElement('meta');
|
||||
viewport.setAttribute('name', 'viewport');
|
||||
}
|
||||
|
||||
// Check if the property already exists
|
||||
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
|
||||
|
||||
if (toggle) {
|
||||
this.cleanupViewport = !hasProperty;
|
||||
|
||||
if (!hasProperty) {
|
||||
viewport.content += `,${property}`;
|
||||
}
|
||||
} else if (this.cleanupViewport) {
|
||||
viewport.content = viewport.content
|
||||
.split(',')
|
||||
.filter((part) => part.trim() !== property)
|
||||
.join(',');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle button and fire events
|
||||
this.onChange();
|
||||
};
|
||||
|
||||
// Trap focus inside container
|
||||
trapFocus = (event) => {
|
||||
// Bail if iOS, not active, not the tab key
|
||||
if (browser.isIos || !this.active || event.key !== 'Tab' || event.keyCode !== 9) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current focused element
|
||||
const focused = document.activeElement;
|
||||
const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');
|
||||
const [first] = focusable;
|
||||
const last = focusable[focusable.length - 1];
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
// Update UI
|
||||
update = () => {
|
||||
if (this.enabled) {
|
||||
let mode;
|
||||
|
||||
if (this.forceFallback) {
|
||||
mode = 'Fallback (forced)';
|
||||
} else if (Fullscreen.native) {
|
||||
mode = 'Native';
|
||||
} else {
|
||||
mode = 'Fallback';
|
||||
}
|
||||
|
||||
this.player.debug.log(`${mode} fullscreen enabled`);
|
||||
} else {
|
||||
this.player.debug.log('Fullscreen not supported and fallback disabled');
|
||||
}
|
||||
|
||||
// Add styling hook to show button
|
||||
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
|
||||
};
|
||||
|
||||
// Make an element fullscreen
|
||||
enter = () => {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// iOS native fullscreen doesn't need the request step
|
||||
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
||||
if (this.player.isVimeo) {
|
||||
this.player.embed.requestFullscreen();
|
||||
} else {
|
||||
this.target.webkitEnterFullscreen();
|
||||
}
|
||||
} else if (!Fullscreen.native || this.forceFallback) {
|
||||
this.toggleFallback(true);
|
||||
} else if (!this.prefix) {
|
||||
this.target.requestFullscreen({ navigationUI: 'hide' });
|
||||
} else if (!is.empty(this.prefix)) {
|
||||
this.target[`${this.prefix}Request${this.property}`]();
|
||||
}
|
||||
};
|
||||
|
||||
// Bail from fullscreen
|
||||
exit = () => {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// iOS native fullscreen
|
||||
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
||||
this.target.webkitExitFullscreen();
|
||||
silencePromise(this.player.play());
|
||||
} else if (!Fullscreen.native || this.forceFallback) {
|
||||
this.toggleFallback(false);
|
||||
} else if (!this.prefix) {
|
||||
(document.cancelFullScreen || document.exitFullscreen).call(document);
|
||||
} else if (!is.empty(this.prefix)) {
|
||||
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
|
||||
document[`${this.prefix}${action}${this.property}`]();
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle state
|
||||
toggle = () => {
|
||||
if (!this.active) {
|
||||
this.enter();
|
||||
} else {
|
||||
this.exit();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Fullscreen;
|
||||
|
||||
245
src/js/html5.js
245
src/js/html5.js
@@ -6,141 +6,142 @@ import support from './support';
|
||||
import { removeElement } from './utils/elements';
|
||||
import { triggerEvent } from './utils/events';
|
||||
import is from './utils/is';
|
||||
import { silencePromise } from './utils/promise';
|
||||
import { setAspectRatio } from './utils/style';
|
||||
|
||||
const html5 = {
|
||||
getSources() {
|
||||
if (!this.isHTML5) {
|
||||
return [];
|
||||
getSources() {
|
||||
if (!this.isHTML5) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sources = Array.from(this.media.querySelectorAll('source'));
|
||||
|
||||
// Filter out unsupported sources (if type is specified)
|
||||
return sources.filter((source) => {
|
||||
const type = source.getAttribute('type');
|
||||
|
||||
if (is.empty(type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return support.mime.call(this, type);
|
||||
});
|
||||
},
|
||||
|
||||
// Get quality levels
|
||||
getQualityOptions() {
|
||||
// Whether we're forcing all options (e.g. for streaming)
|
||||
if (this.config.quality.forced) {
|
||||
return this.config.quality.options;
|
||||
}
|
||||
|
||||
// Get sizes from <source> elements
|
||||
return html5.getSources
|
||||
.call(this)
|
||||
.map((source) => Number(source.getAttribute('size')))
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
setup() {
|
||||
if (!this.isHTML5) {
|
||||
return;
|
||||
}
|
||||
|
||||
const player = this;
|
||||
|
||||
// Set speed options from config
|
||||
player.options.speed = player.config.speed.options;
|
||||
|
||||
// Set aspect ratio if fixed
|
||||
if (!is.empty(this.config.ratio)) {
|
||||
setAspectRatio.call(player);
|
||||
}
|
||||
|
||||
// Quality
|
||||
Object.defineProperty(player.media, 'quality', {
|
||||
get() {
|
||||
// Get sources
|
||||
const sources = html5.getSources.call(player);
|
||||
const source = sources.find((s) => s.getAttribute('src') === player.source);
|
||||
|
||||
// Return size, if match is found
|
||||
return source && Number(source.getAttribute('size'));
|
||||
},
|
||||
set(input) {
|
||||
if (player.quality === input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sources = Array.from(this.media.querySelectorAll('source'));
|
||||
// If we're using an external handler...
|
||||
if (player.config.quality.forced && is.function(player.config.quality.onChange)) {
|
||||
player.config.quality.onChange(input);
|
||||
} else {
|
||||
// Get sources
|
||||
const sources = html5.getSources.call(player);
|
||||
// Get first match for requested size
|
||||
const source = sources.find((s) => Number(s.getAttribute('size')) === input);
|
||||
|
||||
// Filter out unsupported sources (if type is specified)
|
||||
return sources.filter(source => {
|
||||
const type = source.getAttribute('type');
|
||||
|
||||
if (is.empty(type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return support.mime.call(this, type);
|
||||
});
|
||||
},
|
||||
|
||||
// Get quality levels
|
||||
getQualityOptions() {
|
||||
// Whether we're forcing all options (e.g. for streaming)
|
||||
if (this.config.quality.forced) {
|
||||
return this.config.quality.options;
|
||||
}
|
||||
|
||||
// Get sizes from <source> elements
|
||||
return html5.getSources
|
||||
.call(this)
|
||||
.map(source => Number(source.getAttribute('size')))
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
setup() {
|
||||
if (!this.isHTML5) {
|
||||
// No matching source found
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current state
|
||||
const { currentTime, paused, preload, readyState, playbackRate } = player.media;
|
||||
|
||||
// Set new source
|
||||
player.media.src = source.getAttribute('src');
|
||||
|
||||
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
|
||||
if (preload !== 'none' || readyState) {
|
||||
// Restore time
|
||||
player.once('loadedmetadata', () => {
|
||||
player.speed = playbackRate;
|
||||
player.currentTime = currentTime;
|
||||
|
||||
// Resume playing
|
||||
if (!paused) {
|
||||
silencePromise(player.play());
|
||||
}
|
||||
});
|
||||
|
||||
// Load new source
|
||||
player.media.load();
|
||||
}
|
||||
}
|
||||
|
||||
const player = this;
|
||||
|
||||
// Set speed options from config
|
||||
player.options.speed = player.config.speed.options;
|
||||
|
||||
// Set aspect ratio if fixed
|
||||
if (!is.empty(this.config.ratio)) {
|
||||
setAspectRatio.call(player);
|
||||
}
|
||||
|
||||
// Quality
|
||||
Object.defineProperty(player.media, 'quality', {
|
||||
get() {
|
||||
// Get sources
|
||||
const sources = html5.getSources.call(player);
|
||||
const source = sources.find(s => s.getAttribute('src') === player.source);
|
||||
|
||||
// Return size, if match is found
|
||||
return source && Number(source.getAttribute('size'));
|
||||
},
|
||||
set(input) {
|
||||
if (player.quality === input) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're using an an external handler...
|
||||
if (player.config.quality.forced && is.function(player.config.quality.onChange)) {
|
||||
player.config.quality.onChange(input);
|
||||
} else {
|
||||
// Get sources
|
||||
const sources = html5.getSources.call(player);
|
||||
// Get first match for requested size
|
||||
const source = sources.find(s => Number(s.getAttribute('size')) === input);
|
||||
|
||||
// No matching source found
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current state
|
||||
const { currentTime, paused, preload, readyState, playbackRate } = player.media;
|
||||
|
||||
// Set new source
|
||||
player.media.src = source.getAttribute('src');
|
||||
|
||||
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
|
||||
if (preload !== 'none' || readyState) {
|
||||
// Restore time
|
||||
player.once('loadedmetadata', () => {
|
||||
player.speed = playbackRate;
|
||||
player.currentTime = currentTime;
|
||||
|
||||
// Resume playing
|
||||
if (!paused) {
|
||||
player.play();
|
||||
}
|
||||
});
|
||||
|
||||
// Load new source
|
||||
player.media.load();
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger change event
|
||||
triggerEvent.call(player, player.media, 'qualitychange', false, {
|
||||
quality: input,
|
||||
});
|
||||
},
|
||||
// Trigger change event
|
||||
triggerEvent.call(player, player.media, 'qualitychange', false, {
|
||||
quality: input,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// Cancel current network requests
|
||||
// Cancel current network requests
|
||||
// See https://github.com/sampotts/plyr/issues/174
|
||||
cancelRequests() {
|
||||
if (!this.isHTML5) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove child sources
|
||||
removeElement(html5.getSources.call(this));
|
||||
|
||||
// Set blank video src attribute
|
||||
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
|
||||
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
|
||||
this.media.setAttribute('src', this.config.blankVideo);
|
||||
|
||||
// Load the new empty source
|
||||
// This will cancel existing requests
|
||||
// See https://github.com/sampotts/plyr/issues/174
|
||||
cancelRequests() {
|
||||
if (!this.isHTML5) {
|
||||
return;
|
||||
}
|
||||
this.media.load();
|
||||
|
||||
// Remove child sources
|
||||
removeElement(html5.getSources.call(this));
|
||||
|
||||
// Set blank video src attribute
|
||||
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
|
||||
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
|
||||
this.media.setAttribute('src', this.config.blankVideo);
|
||||
|
||||
// Load the new empty source
|
||||
// This will cancel existing requests
|
||||
// See https://github.com/sampotts/plyr/issues/174
|
||||
this.media.load();
|
||||
|
||||
// Debugging
|
||||
this.debug.log('Cancelled network requests');
|
||||
},
|
||||
// Debugging
|
||||
this.debug.log('Cancelled network requests');
|
||||
},
|
||||
};
|
||||
|
||||
export default html5;
|
||||
|
||||
1646
src/js/listeners.js
1646
src/js/listeners.js
File diff suppressed because it is too large
Load Diff
@@ -8,54 +8,52 @@ import youtube from './plugins/youtube';
|
||||
import { createElement, toggleClass, wrap } from './utils/elements';
|
||||
|
||||
const media = {
|
||||
// Setup media
|
||||
setup() {
|
||||
// If there's no media, bail
|
||||
if (!this.media) {
|
||||
this.debug.warn('No media element found!');
|
||||
return;
|
||||
}
|
||||
// Setup media
|
||||
setup() {
|
||||
// If there's no media, bail
|
||||
if (!this.media) {
|
||||
this.debug.warn('No media element found!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add type class
|
||||
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
|
||||
// Add type class
|
||||
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
|
||||
|
||||
// Add provider class
|
||||
toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
|
||||
// Add provider class
|
||||
toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
|
||||
|
||||
// Add video class for embeds
|
||||
// This will require changes if audio embeds are added
|
||||
if (this.isEmbed) {
|
||||
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
|
||||
}
|
||||
// Add video class for embeds
|
||||
// This will require changes if audio embeds are added
|
||||
if (this.isEmbed) {
|
||||
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
|
||||
}
|
||||
|
||||
// Inject the player wrapper
|
||||
if (this.isVideo) {
|
||||
// Create the wrapper div
|
||||
this.elements.wrapper = createElement('div', {
|
||||
class: this.config.classNames.video,
|
||||
});
|
||||
// Inject the player wrapper
|
||||
if (this.isVideo) {
|
||||
// Create the wrapper div
|
||||
this.elements.wrapper = createElement('div', {
|
||||
class: this.config.classNames.video,
|
||||
});
|
||||
|
||||
// Wrap the video in a container
|
||||
wrap(this.media, this.elements.wrapper);
|
||||
// Wrap the video in a container
|
||||
wrap(this.media, this.elements.wrapper);
|
||||
|
||||
// Faux poster container
|
||||
if (this.isEmbed) {
|
||||
this.elements.poster = createElement('div', {
|
||||
class: this.config.classNames.poster,
|
||||
});
|
||||
// Poster image container
|
||||
this.elements.poster = createElement('div', {
|
||||
class: this.config.classNames.poster,
|
||||
});
|
||||
|
||||
this.elements.wrapper.appendChild(this.elements.poster);
|
||||
}
|
||||
}
|
||||
this.elements.wrapper.appendChild(this.elements.poster);
|
||||
}
|
||||
|
||||
if (this.isHTML5) {
|
||||
html5.setup.call(this);
|
||||
} else if (this.isYouTube) {
|
||||
youtube.setup.call(this);
|
||||
} else if (this.isVimeo) {
|
||||
vimeo.setup.call(this);
|
||||
}
|
||||
},
|
||||
if (this.isHTML5) {
|
||||
html5.setup.call(this);
|
||||
} else if (this.isYouTube) {
|
||||
youtube.setup.call(this);
|
||||
} else if (this.isVimeo) {
|
||||
vimeo.setup.call(this);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default media;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -10,393 +10,429 @@ import { triggerEvent } from '../utils/events';
|
||||
import fetch from '../utils/fetch';
|
||||
import is from '../utils/is';
|
||||
import loadScript from '../utils/load-script';
|
||||
import { extend } from '../utils/objects';
|
||||
import { format, stripHTML } from '../utils/strings';
|
||||
import { setAspectRatio } from '../utils/style';
|
||||
import { roundAspectRatio, setAspectRatio } from '../utils/style';
|
||||
import { buildUrlParams } from '../utils/urls';
|
||||
|
||||
// Parse Vimeo ID from URL
|
||||
function parseId(url) {
|
||||
if (is.empty(url)) {
|
||||
return null;
|
||||
}
|
||||
if (is.empty(url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is.number(Number(url))) {
|
||||
return url;
|
||||
}
|
||||
if (is.number(Number(url))) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
|
||||
return url.match(regex) ? RegExp.$2 : url;
|
||||
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
|
||||
return url.match(regex) ? RegExp.$2 : url;
|
||||
}
|
||||
|
||||
// Try to extract a hash for private videos from the URL
|
||||
function parseHash(url) {
|
||||
/* This regex matches a hexadecimal hash if given in any of these forms:
|
||||
* - [https://player.]vimeo.com/video/{id}/{hash}[?params]
|
||||
* - [https://player.]vimeo.com/video/{id}?h={hash}[¶ms]
|
||||
* - [https://player.]vimeo.com/video/{id}?[params]&h={hash}
|
||||
* - video/{id}/{hash}
|
||||
* If matched, the hash is available in capture group 4
|
||||
*/
|
||||
const regex = /^.*(vimeo.com\/|video\/)(\d+)(\?.*&*h=|\/)+([\d,a-f]+)/;
|
||||
const found = url.match(regex);
|
||||
|
||||
return found && found.length === 5 ? found[4] : null;
|
||||
}
|
||||
|
||||
// Set playback state and trigger change (only on actual change)
|
||||
function assurePlaybackState(play) {
|
||||
if (play && !this.embed.hasPlayed) {
|
||||
this.embed.hasPlayed = true;
|
||||
}
|
||||
if (this.media.paused === play) {
|
||||
this.media.paused = !play;
|
||||
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
|
||||
}
|
||||
if (play && !this.embed.hasPlayed) {
|
||||
this.embed.hasPlayed = true;
|
||||
}
|
||||
if (this.media.paused === play) {
|
||||
this.media.paused = !play;
|
||||
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
|
||||
}
|
||||
}
|
||||
|
||||
const vimeo = {
|
||||
setup() {
|
||||
const player = this;
|
||||
setup() {
|
||||
const player = this;
|
||||
|
||||
// Add embed class for responsive
|
||||
toggleClass(player.elements.wrapper, player.config.classNames.embed, true);
|
||||
// Add embed class for responsive
|
||||
toggleClass(player.elements.wrapper, player.config.classNames.embed, true);
|
||||
|
||||
// Set speed options from config
|
||||
player.options.speed = player.config.speed.options;
|
||||
// Set speed options from config
|
||||
player.options.speed = player.config.speed.options;
|
||||
|
||||
// Set intial ratio
|
||||
setAspectRatio.call(player);
|
||||
// Set intial ratio
|
||||
setAspectRatio.call(player);
|
||||
|
||||
// Load the SDK if not already
|
||||
if (!is.object(window.Vimeo)) {
|
||||
loadScript(player.config.urls.vimeo.sdk)
|
||||
.then(() => {
|
||||
vimeo.ready.call(player);
|
||||
})
|
||||
.catch(error => {
|
||||
player.debug.warn('Vimeo SDK (player.js) failed to load', error);
|
||||
});
|
||||
} else {
|
||||
vimeo.ready.call(player);
|
||||
}
|
||||
},
|
||||
// Load the SDK if not already
|
||||
if (!is.object(window.Vimeo)) {
|
||||
loadScript(player.config.urls.vimeo.sdk)
|
||||
.then(() => {
|
||||
vimeo.ready.call(player);
|
||||
})
|
||||
.catch((error) => {
|
||||
player.debug.warn('Vimeo SDK (player.js) failed to load', error);
|
||||
});
|
||||
} else {
|
||||
vimeo.ready.call(player);
|
||||
}
|
||||
},
|
||||
|
||||
// API Ready
|
||||
ready() {
|
||||
const player = this;
|
||||
const config = player.config.vimeo;
|
||||
// API Ready
|
||||
ready() {
|
||||
const player = this;
|
||||
const config = player.config.vimeo;
|
||||
const { premium, referrerPolicy, ...frameParams } = config;
|
||||
// Get the source URL or ID
|
||||
let source = player.media.getAttribute('src');
|
||||
let hash = '';
|
||||
// Get from <div> if needed
|
||||
if (is.empty(source)) {
|
||||
source = player.media.getAttribute(player.config.attributes.embed.id);
|
||||
// hash can also be set as attribute on the <div>
|
||||
hash = player.media.getAttribute(player.config.attributes.embed.hash);
|
||||
} else {
|
||||
hash = parseHash(source);
|
||||
}
|
||||
const hashParam = hash ? { h: hash } : {};
|
||||
|
||||
// Get Vimeo params for the iframe
|
||||
const params = buildUrlParams(
|
||||
extend(
|
||||
{},
|
||||
{
|
||||
loop: player.config.loop.active,
|
||||
autoplay: player.autoplay,
|
||||
muted: player.muted,
|
||||
gesture: 'media',
|
||||
playsinline: !this.config.fullscreen.iosNative,
|
||||
},
|
||||
config,
|
||||
),
|
||||
);
|
||||
// If the owner has a pro or premium account then we can hide controls etc
|
||||
if (premium) {
|
||||
Object.assign(frameParams, {
|
||||
controls: false,
|
||||
sidedock: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Get the source URL or ID
|
||||
let source = player.media.getAttribute('src');
|
||||
// Get Vimeo params for the iframe
|
||||
const params = buildUrlParams({
|
||||
loop: player.config.loop.active,
|
||||
autoplay: player.autoplay,
|
||||
muted: player.muted,
|
||||
gesture: 'media',
|
||||
playsinline: !this.config.fullscreen.iosNative,
|
||||
// hash has to be added to iframe-URL
|
||||
...hashParam,
|
||||
...frameParams,
|
||||
});
|
||||
|
||||
// Get from <div> if needed
|
||||
if (is.empty(source)) {
|
||||
source = player.media.getAttribute(player.config.attributes.embed.id);
|
||||
const id = parseId(source);
|
||||
// Build an iframe
|
||||
const iframe = createElement('iframe');
|
||||
const src = format(player.config.urls.vimeo.iframe, id, params);
|
||||
iframe.setAttribute('src', src);
|
||||
iframe.setAttribute('allowfullscreen', '');
|
||||
iframe.setAttribute(
|
||||
'allow',
|
||||
['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '),
|
||||
);
|
||||
|
||||
// Set the referrer policy if required
|
||||
if (!is.empty(referrerPolicy)) {
|
||||
iframe.setAttribute('referrerPolicy', referrerPolicy);
|
||||
}
|
||||
|
||||
// Inject the package
|
||||
if (premium || !config.customControls) {
|
||||
iframe.setAttribute('data-poster', player.poster);
|
||||
player.media = replaceElement(iframe, player.media);
|
||||
} else {
|
||||
const wrapper = createElement('div', {
|
||||
class: player.config.classNames.embedContainer,
|
||||
'data-poster': player.poster,
|
||||
});
|
||||
wrapper.appendChild(iframe);
|
||||
player.media = replaceElement(wrapper, player.media);
|
||||
}
|
||||
|
||||
// Get poster image
|
||||
if (!config.customControls) {
|
||||
fetch(format(player.config.urls.vimeo.api, src)).then((response) => {
|
||||
if (is.empty(response) || !response.thumbnail_url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = parseId(source);
|
||||
// Build an iframe
|
||||
const iframe = createElement('iframe');
|
||||
const src = format(player.config.urls.vimeo.iframe, id, params);
|
||||
iframe.setAttribute('src', src);
|
||||
iframe.setAttribute('allowfullscreen', '');
|
||||
iframe.setAttribute('allowtransparency', '');
|
||||
iframe.setAttribute('allow', 'autoplay');
|
||||
// Set and show poster
|
||||
ui.setPoster.call(player, response.thumbnail_url).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
// Set the referrer policy if required
|
||||
if (!is.empty(config.referrerPolicy)) {
|
||||
iframe.setAttribute('referrerPolicy', config.referrerPolicy);
|
||||
}
|
||||
// Setup instance
|
||||
// https://github.com/vimeo/player.js
|
||||
player.embed = new window.Vimeo.Player(iframe, {
|
||||
autopause: player.config.autopause,
|
||||
muted: player.muted,
|
||||
});
|
||||
|
||||
// Get poster, if already set
|
||||
const { poster } = player;
|
||||
// Inject the package
|
||||
const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
|
||||
wrapper.appendChild(iframe);
|
||||
player.media = replaceElement(wrapper, player.media);
|
||||
player.media.paused = true;
|
||||
player.media.currentTime = 0;
|
||||
|
||||
// Get poster image
|
||||
fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
|
||||
if (is.empty(response)) {
|
||||
return;
|
||||
}
|
||||
// Disable native text track rendering
|
||||
if (player.supported.ui) {
|
||||
player.embed.disableTextTrack();
|
||||
}
|
||||
|
||||
// Get the URL for thumbnail
|
||||
const url = new URL(response[0].thumbnail_large);
|
||||
// Create a faux HTML5 API using the Vimeo API
|
||||
player.media.play = () => {
|
||||
assurePlaybackState.call(player, true);
|
||||
return player.embed.play();
|
||||
};
|
||||
|
||||
// Get original image
|
||||
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
|
||||
player.media.pause = () => {
|
||||
assurePlaybackState.call(player, false);
|
||||
return player.embed.pause();
|
||||
};
|
||||
|
||||
// Set and show poster
|
||||
ui.setPoster.call(player, url.href).catch(() => {});
|
||||
});
|
||||
player.media.stop = () => {
|
||||
player.pause();
|
||||
player.currentTime = 0;
|
||||
};
|
||||
|
||||
// Setup instance
|
||||
// https://github.com/vimeo/player.js
|
||||
player.embed = new window.Vimeo.Player(iframe, {
|
||||
autopause: player.config.autopause,
|
||||
muted: player.muted,
|
||||
});
|
||||
// Seeking
|
||||
let { currentTime } = player.media;
|
||||
Object.defineProperty(player.media, 'currentTime', {
|
||||
get() {
|
||||
return currentTime;
|
||||
},
|
||||
set(time) {
|
||||
// Vimeo will automatically play on seek if the video hasn't been played before
|
||||
|
||||
player.media.paused = true;
|
||||
player.media.currentTime = 0;
|
||||
// Get current paused state and volume etc
|
||||
const { embed, media, paused, volume } = player;
|
||||
const restorePause = paused && !embed.hasPlayed;
|
||||
|
||||
// Disable native text track rendering
|
||||
if (player.supported.ui) {
|
||||
player.embed.disableTextTrack();
|
||||
}
|
||||
// Set seeking state and trigger event
|
||||
media.seeking = true;
|
||||
triggerEvent.call(player, media, 'seeking');
|
||||
|
||||
// Create a faux HTML5 API using the Vimeo API
|
||||
player.media.play = () => {
|
||||
assurePlaybackState.call(player, true);
|
||||
return player.embed.play();
|
||||
};
|
||||
// If paused, mute until seek is complete
|
||||
Promise.resolve(restorePause && embed.setVolume(0))
|
||||
// Seek
|
||||
.then(() => embed.setCurrentTime(time))
|
||||
// Restore paused
|
||||
.then(() => restorePause && embed.pause())
|
||||
// Restore volume
|
||||
.then(() => restorePause && embed.setVolume(volume))
|
||||
.catch(() => {
|
||||
// Do nothing
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
player.media.pause = () => {
|
||||
assurePlaybackState.call(player, false);
|
||||
return player.embed.pause();
|
||||
};
|
||||
|
||||
player.media.stop = () => {
|
||||
player.pause();
|
||||
player.currentTime = 0;
|
||||
};
|
||||
|
||||
// Seeking
|
||||
let { currentTime } = player.media;
|
||||
Object.defineProperty(player.media, 'currentTime', {
|
||||
get() {
|
||||
return currentTime;
|
||||
},
|
||||
set(time) {
|
||||
// Vimeo will automatically play on seek if the video hasn't been played before
|
||||
|
||||
// Get current paused state and volume etc
|
||||
const { embed, media, paused, volume } = player;
|
||||
const restorePause = paused && !embed.hasPlayed;
|
||||
|
||||
// Set seeking state and trigger event
|
||||
media.seeking = true;
|
||||
triggerEvent.call(player, media, 'seeking');
|
||||
|
||||
// If paused, mute until seek is complete
|
||||
Promise.resolve(restorePause && embed.setVolume(0))
|
||||
// Seek
|
||||
.then(() => embed.setCurrentTime(time))
|
||||
// Restore paused
|
||||
.then(() => restorePause && embed.pause())
|
||||
// Restore volume
|
||||
.then(() => restorePause && embed.setVolume(volume))
|
||||
.catch(() => {
|
||||
// Do nothing
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Playback speed
|
||||
let speed = player.config.speed.selected;
|
||||
Object.defineProperty(player.media, 'playbackRate', {
|
||||
get() {
|
||||
return speed;
|
||||
},
|
||||
set(input) {
|
||||
player.embed.setPlaybackRate(input).then(() => {
|
||||
speed = input;
|
||||
triggerEvent.call(player, player.media, 'ratechange');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Volume
|
||||
let { volume } = player.config;
|
||||
Object.defineProperty(player.media, 'volume', {
|
||||
get() {
|
||||
return volume;
|
||||
},
|
||||
set(input) {
|
||||
player.embed.setVolume(input).then(() => {
|
||||
volume = input;
|
||||
triggerEvent.call(player, player.media, 'volumechange');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Muted
|
||||
let { muted } = player.config;
|
||||
Object.defineProperty(player.media, 'muted', {
|
||||
get() {
|
||||
return muted;
|
||||
},
|
||||
set(input) {
|
||||
const toggle = is.boolean(input) ? input : false;
|
||||
|
||||
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
|
||||
muted = toggle;
|
||||
triggerEvent.call(player, player.media, 'volumechange');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Loop
|
||||
let { loop } = player.config;
|
||||
Object.defineProperty(player.media, 'loop', {
|
||||
get() {
|
||||
return loop;
|
||||
},
|
||||
set(input) {
|
||||
const toggle = is.boolean(input) ? input : player.config.loop.active;
|
||||
|
||||
player.embed.setLoop(toggle).then(() => {
|
||||
loop = toggle;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Source
|
||||
let currentSrc;
|
||||
// Playback speed
|
||||
let speed = player.config.speed.selected;
|
||||
Object.defineProperty(player.media, 'playbackRate', {
|
||||
get() {
|
||||
return speed;
|
||||
},
|
||||
set(input) {
|
||||
player.embed
|
||||
.getVideoUrl()
|
||||
.then(value => {
|
||||
currentSrc = value;
|
||||
controls.setDownloadUrl.call(player);
|
||||
})
|
||||
.catch(error => {
|
||||
this.debug.warn(error);
|
||||
});
|
||||
.setPlaybackRate(input)
|
||||
.then(() => {
|
||||
speed = input;
|
||||
triggerEvent.call(player, player.media, 'ratechange');
|
||||
})
|
||||
.catch(() => {
|
||||
// Cannot set Playback Rate, Video is probably not on Pro account
|
||||
player.options.speed = [1];
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(player.media, 'currentSrc', {
|
||||
get() {
|
||||
return currentSrc;
|
||||
},
|
||||
// Volume
|
||||
let { volume } = player.config;
|
||||
Object.defineProperty(player.media, 'volume', {
|
||||
get() {
|
||||
return volume;
|
||||
},
|
||||
set(input) {
|
||||
player.embed.setVolume(input).then(() => {
|
||||
volume = input;
|
||||
triggerEvent.call(player, player.media, 'volumechange');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Ended
|
||||
Object.defineProperty(player.media, 'ended', {
|
||||
get() {
|
||||
return player.currentTime === player.duration;
|
||||
},
|
||||
// Muted
|
||||
let { muted } = player.config;
|
||||
Object.defineProperty(player.media, 'muted', {
|
||||
get() {
|
||||
return muted;
|
||||
},
|
||||
set(input) {
|
||||
const toggle = is.boolean(input) ? input : false;
|
||||
|
||||
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
|
||||
muted = toggle;
|
||||
triggerEvent.call(player, player.media, 'volumechange');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Set aspect ratio based on video size
|
||||
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
|
||||
const [width, height] = dimensions;
|
||||
player.embed.ratio = [width, height];
|
||||
setAspectRatio.call(this);
|
||||
// Loop
|
||||
let { loop } = player.config;
|
||||
Object.defineProperty(player.media, 'loop', {
|
||||
get() {
|
||||
return loop;
|
||||
},
|
||||
set(input) {
|
||||
const toggle = is.boolean(input) ? input : player.config.loop.active;
|
||||
|
||||
player.embed.setLoop(toggle).then(() => {
|
||||
loop = toggle;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Set autopause
|
||||
player.embed.setAutopause(player.config.autopause).then(state => {
|
||||
player.config.autopause = state;
|
||||
});
|
||||
// Source
|
||||
let currentSrc;
|
||||
player.embed
|
||||
.getVideoUrl()
|
||||
.then((value) => {
|
||||
currentSrc = value;
|
||||
controls.setDownloadUrl.call(player);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.debug.warn(error);
|
||||
});
|
||||
|
||||
// Get title
|
||||
player.embed.getVideoTitle().then(title => {
|
||||
player.config.title = title;
|
||||
ui.setTitle.call(this);
|
||||
});
|
||||
Object.defineProperty(player.media, 'currentSrc', {
|
||||
get() {
|
||||
return currentSrc;
|
||||
},
|
||||
});
|
||||
|
||||
// Get current time
|
||||
player.embed.getCurrentTime().then(value => {
|
||||
currentTime = value;
|
||||
triggerEvent.call(player, player.media, 'timeupdate');
|
||||
});
|
||||
// Ended
|
||||
Object.defineProperty(player.media, 'ended', {
|
||||
get() {
|
||||
return player.currentTime === player.duration;
|
||||
},
|
||||
});
|
||||
|
||||
// Get duration
|
||||
player.embed.getDuration().then(value => {
|
||||
player.media.duration = value;
|
||||
triggerEvent.call(player, player.media, 'durationchange');
|
||||
});
|
||||
// Set aspect ratio based on video size
|
||||
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then((dimensions) => {
|
||||
const [width, height] = dimensions;
|
||||
player.embed.ratio = roundAspectRatio(width, height);
|
||||
setAspectRatio.call(this);
|
||||
});
|
||||
|
||||
// Get captions
|
||||
player.embed.getTextTracks().then(tracks => {
|
||||
player.media.textTracks = tracks;
|
||||
captions.setup.call(player);
|
||||
});
|
||||
// Set autopause
|
||||
player.embed.setAutopause(player.config.autopause).then((state) => {
|
||||
player.config.autopause = state;
|
||||
});
|
||||
|
||||
player.embed.on('cuechange', ({ cues = [] }) => {
|
||||
const strippedCues = cues.map(cue => stripHTML(cue.text));
|
||||
captions.updateCues.call(player, strippedCues);
|
||||
});
|
||||
// Get title
|
||||
player.embed.getVideoTitle().then((title) => {
|
||||
player.config.title = title;
|
||||
ui.setTitle.call(this);
|
||||
});
|
||||
|
||||
player.embed.on('loaded', () => {
|
||||
// Assure state and events are updated on autoplay
|
||||
player.embed.getPaused().then(paused => {
|
||||
assurePlaybackState.call(player, !paused);
|
||||
if (!paused) {
|
||||
triggerEvent.call(player, player.media, 'playing');
|
||||
}
|
||||
});
|
||||
// Get current time
|
||||
player.embed.getCurrentTime().then((value) => {
|
||||
currentTime = value;
|
||||
triggerEvent.call(player, player.media, 'timeupdate');
|
||||
});
|
||||
|
||||
if (is.element(player.embed.element) && player.supported.ui) {
|
||||
const frame = player.embed.element;
|
||||
// Get duration
|
||||
player.embed.getDuration().then((value) => {
|
||||
player.media.duration = value;
|
||||
triggerEvent.call(player, player.media, 'durationchange');
|
||||
});
|
||||
|
||||
// Fix keyboard focus issues
|
||||
// https://github.com/sampotts/plyr/issues/317
|
||||
frame.setAttribute('tabindex', -1);
|
||||
}
|
||||
});
|
||||
// Get captions
|
||||
player.embed.getTextTracks().then((tracks) => {
|
||||
player.media.textTracks = tracks;
|
||||
captions.setup.call(player);
|
||||
});
|
||||
|
||||
player.embed.on('bufferstart', () => {
|
||||
triggerEvent.call(player, player.media, 'waiting');
|
||||
});
|
||||
player.embed.on('cuechange', ({ cues = [] }) => {
|
||||
const strippedCues = cues.map((cue) => stripHTML(cue.text));
|
||||
captions.updateCues.call(player, strippedCues);
|
||||
});
|
||||
|
||||
player.embed.on('bufferend', () => {
|
||||
triggerEvent.call(player, player.media, 'playing');
|
||||
});
|
||||
player.embed.on('loaded', () => {
|
||||
// Assure state and events are updated on autoplay
|
||||
player.embed.getPaused().then((paused) => {
|
||||
assurePlaybackState.call(player, !paused);
|
||||
if (!paused) {
|
||||
triggerEvent.call(player, player.media, 'playing');
|
||||
}
|
||||
});
|
||||
|
||||
player.embed.on('play', () => {
|
||||
assurePlaybackState.call(player, true);
|
||||
triggerEvent.call(player, player.media, 'playing');
|
||||
});
|
||||
if (is.element(player.embed.element) && player.supported.ui) {
|
||||
const frame = player.embed.element;
|
||||
|
||||
player.embed.on('pause', () => {
|
||||
assurePlaybackState.call(player, false);
|
||||
});
|
||||
// Fix keyboard focus issues
|
||||
// https://github.com/sampotts/plyr/issues/317
|
||||
frame.setAttribute('tabindex', -1);
|
||||
}
|
||||
});
|
||||
|
||||
player.embed.on('timeupdate', data => {
|
||||
player.media.seeking = false;
|
||||
currentTime = data.seconds;
|
||||
triggerEvent.call(player, player.media, 'timeupdate');
|
||||
});
|
||||
player.embed.on('bufferstart', () => {
|
||||
triggerEvent.call(player, player.media, 'waiting');
|
||||
});
|
||||
|
||||
player.embed.on('progress', data => {
|
||||
player.media.buffered = data.percent;
|
||||
triggerEvent.call(player, player.media, 'progress');
|
||||
player.embed.on('bufferend', () => {
|
||||
triggerEvent.call(player, player.media, 'playing');
|
||||
});
|
||||
|
||||
// Check all loaded
|
||||
if (parseInt(data.percent, 10) === 1) {
|
||||
triggerEvent.call(player, player.media, 'canplaythrough');
|
||||
}
|
||||
player.embed.on('play', () => {
|
||||
assurePlaybackState.call(player, true);
|
||||
triggerEvent.call(player, player.media, 'playing');
|
||||
});
|
||||
|
||||
// Get duration as if we do it before load, it gives an incorrect value
|
||||
// https://github.com/sampotts/plyr/issues/891
|
||||
player.embed.getDuration().then(value => {
|
||||
if (value !== player.media.duration) {
|
||||
player.media.duration = value;
|
||||
triggerEvent.call(player, player.media, 'durationchange');
|
||||
}
|
||||
});
|
||||
});
|
||||
player.embed.on('pause', () => {
|
||||
assurePlaybackState.call(player, false);
|
||||
});
|
||||
|
||||
player.embed.on('seeked', () => {
|
||||
player.media.seeking = false;
|
||||
triggerEvent.call(player, player.media, 'seeked');
|
||||
});
|
||||
player.embed.on('timeupdate', (data) => {
|
||||
player.media.seeking = false;
|
||||
currentTime = data.seconds;
|
||||
triggerEvent.call(player, player.media, 'timeupdate');
|
||||
});
|
||||
|
||||
player.embed.on('ended', () => {
|
||||
player.media.paused = true;
|
||||
triggerEvent.call(player, player.media, 'ended');
|
||||
});
|
||||
player.embed.on('progress', (data) => {
|
||||
player.media.buffered = data.percent;
|
||||
triggerEvent.call(player, player.media, 'progress');
|
||||
|
||||
player.embed.on('error', detail => {
|
||||
player.media.error = detail;
|
||||
triggerEvent.call(player, player.media, 'error');
|
||||
});
|
||||
// Check all loaded
|
||||
if (parseInt(data.percent, 10) === 1) {
|
||||
triggerEvent.call(player, player.media, 'canplaythrough');
|
||||
}
|
||||
|
||||
// Rebuild UI
|
||||
setTimeout(() => ui.build.call(player), 0);
|
||||
},
|
||||
// Get duration as if we do it before load, it gives an incorrect value
|
||||
// https://github.com/sampotts/plyr/issues/891
|
||||
player.embed.getDuration().then((value) => {
|
||||
if (value !== player.media.duration) {
|
||||
player.media.duration = value;
|
||||
triggerEvent.call(player, player.media, 'durationchange');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
player.embed.on('seeked', () => {
|
||||
player.media.seeking = false;
|
||||
triggerEvent.call(player, player.media, 'seeked');
|
||||
});
|
||||
|
||||
player.embed.on('ended', () => {
|
||||
player.media.paused = true;
|
||||
triggerEvent.call(player, player.media, 'ended');
|
||||
});
|
||||
|
||||
player.embed.on('error', (detail) => {
|
||||
player.media.error = detail;
|
||||
triggerEvent.call(player, player.media, 'error');
|
||||
});
|
||||
|
||||
// Rebuild UI
|
||||
if (config.customControls) {
|
||||
setTimeout(() => ui.build.call(player), 0);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default vimeo;
|
||||
|
||||
@@ -11,430 +11,437 @@ import loadImage from '../utils/load-image';
|
||||
import loadScript from '../utils/load-script';
|
||||
import { extend } from '../utils/objects';
|
||||
import { format, generateId } from '../utils/strings';
|
||||
import { setAspectRatio } from '../utils/style';
|
||||
import { roundAspectRatio, setAspectRatio } from '../utils/style';
|
||||
|
||||
// Parse YouTube ID from URL
|
||||
function parseId(url) {
|
||||
if (is.empty(url)) {
|
||||
return null;
|
||||
}
|
||||
if (is.empty(url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
|
||||
return url.match(regex) ? RegExp.$2 : url;
|
||||
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
|
||||
return url.match(regex) ? RegExp.$2 : url;
|
||||
}
|
||||
|
||||
// Set playback state and trigger change (only on actual change)
|
||||
function assurePlaybackState(play) {
|
||||
if (play && !this.embed.hasPlayed) {
|
||||
this.embed.hasPlayed = true;
|
||||
}
|
||||
if (this.media.paused === play) {
|
||||
this.media.paused = !play;
|
||||
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
|
||||
}
|
||||
if (play && !this.embed.hasPlayed) {
|
||||
this.embed.hasPlayed = true;
|
||||
}
|
||||
if (this.media.paused === play) {
|
||||
this.media.paused = !play;
|
||||
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
|
||||
}
|
||||
}
|
||||
|
||||
function getHost(config) {
|
||||
if (config.noCookie) {
|
||||
return 'https://www.youtube-nocookie.com';
|
||||
}
|
||||
if (config.noCookie) {
|
||||
return 'https://www.youtube-nocookie.com';
|
||||
}
|
||||
|
||||
if (window.location.protocol === 'http:') {
|
||||
return 'http://www.youtube.com';
|
||||
}
|
||||
if (window.location.protocol === 'http:') {
|
||||
return 'http://www.youtube.com';
|
||||
}
|
||||
|
||||
// Use YouTube's default
|
||||
return undefined;
|
||||
// Use YouTube's default
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const youtube = {
|
||||
setup() {
|
||||
// Add embed class for responsive
|
||||
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
|
||||
setup() {
|
||||
// Add embed class for responsive
|
||||
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
|
||||
|
||||
// Setup API
|
||||
if (is.object(window.YT) && is.function(window.YT.Player)) {
|
||||
youtube.ready.call(this);
|
||||
} else {
|
||||
// Reference current global callback
|
||||
const callback = window.onYouTubeIframeAPIReady;
|
||||
// Setup API
|
||||
if (is.object(window.YT) && is.function(window.YT.Player)) {
|
||||
youtube.ready.call(this);
|
||||
} else {
|
||||
// Reference current global callback
|
||||
const callback = window.onYouTubeIframeAPIReady;
|
||||
|
||||
// Set callback to process queue
|
||||
window.onYouTubeIframeAPIReady = () => {
|
||||
// Call global callback if set
|
||||
if (is.function(callback)) {
|
||||
callback();
|
||||
}
|
||||
|
||||
youtube.ready.call(this);
|
||||
};
|
||||
|
||||
// Load the SDK
|
||||
loadScript(this.config.urls.youtube.sdk).catch(error => {
|
||||
this.debug.warn('YouTube API failed to load', error);
|
||||
});
|
||||
// Set callback to process queue
|
||||
window.onYouTubeIframeAPIReady = () => {
|
||||
// Call global callback if set
|
||||
if (is.function(callback)) {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
// Get the media title
|
||||
getTitle(videoId) {
|
||||
const url = format(this.config.urls.youtube.api, videoId);
|
||||
youtube.ready.call(this);
|
||||
};
|
||||
|
||||
fetch(url)
|
||||
.then(data => {
|
||||
if (is.object(data)) {
|
||||
const { title, height, width } = data;
|
||||
// Load the SDK
|
||||
loadScript(this.config.urls.youtube.sdk).catch((error) => {
|
||||
this.debug.warn('YouTube API failed to load', error);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Set title
|
||||
this.config.title = title;
|
||||
ui.setTitle.call(this);
|
||||
// Get the media title
|
||||
getTitle(videoId) {
|
||||
const url = format(this.config.urls.youtube.api, videoId);
|
||||
|
||||
// Set aspect ratio
|
||||
this.embed.ratio = [width, height];
|
||||
}
|
||||
fetch(url)
|
||||
.then((data) => {
|
||||
if (is.object(data)) {
|
||||
const { title, height, width } = data;
|
||||
|
||||
setAspectRatio.call(this);
|
||||
})
|
||||
.catch(() => {
|
||||
// Set aspect ratio
|
||||
setAspectRatio.call(this);
|
||||
});
|
||||
},
|
||||
// Set title
|
||||
this.config.title = title;
|
||||
ui.setTitle.call(this);
|
||||
|
||||
// API ready
|
||||
ready() {
|
||||
const player = this;
|
||||
// Ignore already setup (race condition)
|
||||
const currentId = player.media && player.media.getAttribute('id');
|
||||
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
|
||||
// Set aspect ratio
|
||||
this.embed.ratio = roundAspectRatio(width, height);
|
||||
}
|
||||
|
||||
setAspectRatio.call(this);
|
||||
})
|
||||
.catch(() => {
|
||||
// Set aspect ratio
|
||||
setAspectRatio.call(this);
|
||||
});
|
||||
},
|
||||
|
||||
// API ready
|
||||
ready() {
|
||||
const player = this;
|
||||
const config = player.config.youtube;
|
||||
// Ignore already setup (race condition)
|
||||
const currentId = player.media && player.media.getAttribute('id');
|
||||
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the source URL or ID
|
||||
let source = player.media.getAttribute('src');
|
||||
|
||||
// Get from <div> if needed
|
||||
if (is.empty(source)) {
|
||||
source = player.media.getAttribute(this.config.attributes.embed.id);
|
||||
}
|
||||
|
||||
// Replace the <iframe> with a <div> due to YouTube API issues
|
||||
const videoId = parseId(source);
|
||||
const id = generateId(player.provider);
|
||||
// Replace media element
|
||||
const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });
|
||||
player.media = replaceElement(container, player.media);
|
||||
|
||||
// Only load the poster when using custom controls
|
||||
if (config.customControls) {
|
||||
const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
|
||||
|
||||
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
|
||||
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
|
||||
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
|
||||
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
|
||||
.then((image) => ui.setPoster.call(player, image.src))
|
||||
.then((src) => {
|
||||
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
|
||||
if (!src.includes('maxres')) {
|
||||
player.elements.poster.style.backgroundSize = 'cover';
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// Setup instance
|
||||
// https://developers.google.com/youtube/iframe_api_reference
|
||||
player.embed = new window.YT.Player(player.media, {
|
||||
videoId,
|
||||
host: getHost(config),
|
||||
playerVars: extend(
|
||||
{},
|
||||
{
|
||||
// Autoplay
|
||||
autoplay: player.config.autoplay ? 1 : 0,
|
||||
// iframe interface language
|
||||
hl: player.config.hl,
|
||||
// Only show controls if not fully supported or opted out
|
||||
controls: player.supported.ui && config.customControls ? 0 : 1,
|
||||
// Disable keyboard as we handle it
|
||||
disablekb: 1,
|
||||
// Allow iOS inline playback
|
||||
playsinline: !player.config.fullscreen.iosNative ? 1 : 0,
|
||||
// Captions are flaky on YouTube
|
||||
cc_load_policy: player.captions.active ? 1 : 0,
|
||||
cc_lang_pref: player.config.captions.language,
|
||||
// Tracking for stats
|
||||
widget_referrer: window ? window.location.href : null,
|
||||
},
|
||||
config,
|
||||
),
|
||||
events: {
|
||||
onError(event) {
|
||||
// YouTube may fire onError twice, so only handle it once
|
||||
if (!player.media.error) {
|
||||
const code = event.data;
|
||||
// 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');
|
||||
}
|
||||
},
|
||||
onPlaybackRateChange(event) {
|
||||
// Get the instance
|
||||
const instance = event.target;
|
||||
|
||||
// Get current speed
|
||||
player.media.playbackRate = instance.getPlaybackRate();
|
||||
|
||||
triggerEvent.call(player, player.media, 'ratechange');
|
||||
},
|
||||
onReady(event) {
|
||||
// Bail if onReady has already been called. See issue #1108
|
||||
if (is.function(player.media.play)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Get the instance
|
||||
const instance = event.target;
|
||||
|
||||
// Get the source URL or ID
|
||||
let source = player.media.getAttribute('src');
|
||||
// Get the title
|
||||
youtube.getTitle.call(player, videoId);
|
||||
|
||||
// Get from <div> if needed
|
||||
if (is.empty(source)) {
|
||||
source = player.media.getAttribute(this.config.attributes.embed.id);
|
||||
}
|
||||
// Create a faux HTML5 API using the YouTube API
|
||||
player.media.play = () => {
|
||||
assurePlaybackState.call(player, true);
|
||||
instance.playVideo();
|
||||
};
|
||||
|
||||
// Replace the <iframe> with a <div> due to YouTube API issues
|
||||
const videoId = parseId(source);
|
||||
const id = generateId(player.provider);
|
||||
// Get poster, if already set
|
||||
const { poster } = player;
|
||||
// Replace media element
|
||||
const container = createElement('div', { id, poster });
|
||||
player.media = replaceElement(container, player.media);
|
||||
player.media.pause = () => {
|
||||
assurePlaybackState.call(player, false);
|
||||
instance.pauseVideo();
|
||||
};
|
||||
|
||||
// Id to poster wrapper
|
||||
const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
|
||||
player.media.stop = () => {
|
||||
instance.stopVideo();
|
||||
};
|
||||
|
||||
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
|
||||
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
|
||||
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
|
||||
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
|
||||
.then(image => ui.setPoster.call(player, image.src))
|
||||
.then(src => {
|
||||
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
|
||||
if (!src.includes('maxres')) {
|
||||
player.elements.poster.style.backgroundSize = 'cover';
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
player.media.duration = instance.getDuration();
|
||||
player.media.paused = true;
|
||||
|
||||
const config = player.config.youtube;
|
||||
|
||||
// Setup instance
|
||||
// https://developers.google.com/youtube/iframe_api_reference
|
||||
player.embed = new window.YT.Player(id, {
|
||||
videoId,
|
||||
host: getHost(config),
|
||||
playerVars: extend(
|
||||
{},
|
||||
{
|
||||
autoplay: player.config.autoplay ? 1 : 0, // Autoplay
|
||||
hl: player.config.hl, // iframe interface language
|
||||
controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported
|
||||
disablekb: 1, // Disable keyboard as we handle it
|
||||
playsinline: !player.config.fullscreen.iosNative ? 1 : 0, // Allow iOS inline playback
|
||||
// Captions are flaky on YouTube
|
||||
cc_load_policy: player.captions.active ? 1 : 0,
|
||||
cc_lang_pref: player.config.captions.language,
|
||||
// Tracking for stats
|
||||
widget_referrer: window ? window.location.href : null,
|
||||
},
|
||||
config,
|
||||
),
|
||||
events: {
|
||||
onError(event) {
|
||||
// YouTube may fire onError twice, so only handle it once
|
||||
if (!player.media.error) {
|
||||
const code = event.data;
|
||||
// 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');
|
||||
}
|
||||
},
|
||||
onPlaybackRateChange(event) {
|
||||
// Get the instance
|
||||
const instance = event.target;
|
||||
|
||||
// Get current speed
|
||||
player.media.playbackRate = instance.getPlaybackRate();
|
||||
|
||||
triggerEvent.call(player, player.media, 'ratechange');
|
||||
},
|
||||
onReady(event) {
|
||||
// Bail if onReady has already been called. See issue #1108
|
||||
if (is.function(player.media.play)) {
|
||||
return;
|
||||
}
|
||||
// Get the instance
|
||||
const instance = event.target;
|
||||
|
||||
// Get the title
|
||||
youtube.getTitle.call(player, videoId);
|
||||
|
||||
// Create a faux HTML5 API using the YouTube API
|
||||
player.media.play = () => {
|
||||
assurePlaybackState.call(player, true);
|
||||
instance.playVideo();
|
||||
};
|
||||
|
||||
player.media.pause = () => {
|
||||
assurePlaybackState.call(player, false);
|
||||
instance.pauseVideo();
|
||||
};
|
||||
|
||||
player.media.stop = () => {
|
||||
instance.stopVideo();
|
||||
};
|
||||
|
||||
player.media.duration = instance.getDuration();
|
||||
player.media.paused = true;
|
||||
|
||||
// Seeking
|
||||
player.media.currentTime = 0;
|
||||
Object.defineProperty(player.media, 'currentTime', {
|
||||
get() {
|
||||
return Number(instance.getCurrentTime());
|
||||
},
|
||||
set(time) {
|
||||
// If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
|
||||
if (player.paused && !player.embed.hasPlayed) {
|
||||
player.embed.mute();
|
||||
}
|
||||
|
||||
// Set seeking state and trigger event
|
||||
player.media.seeking = true;
|
||||
triggerEvent.call(player, player.media, 'seeking');
|
||||
|
||||
// Seek after events sent
|
||||
instance.seekTo(time);
|
||||
},
|
||||
});
|
||||
|
||||
// Playback speed
|
||||
Object.defineProperty(player.media, 'playbackRate', {
|
||||
get() {
|
||||
return instance.getPlaybackRate();
|
||||
},
|
||||
set(input) {
|
||||
instance.setPlaybackRate(input);
|
||||
},
|
||||
});
|
||||
|
||||
// Volume
|
||||
let { volume } = player.config;
|
||||
Object.defineProperty(player.media, 'volume', {
|
||||
get() {
|
||||
return volume;
|
||||
},
|
||||
set(input) {
|
||||
volume = input;
|
||||
instance.setVolume(volume * 100);
|
||||
triggerEvent.call(player, player.media, 'volumechange');
|
||||
},
|
||||
});
|
||||
|
||||
// Muted
|
||||
let { muted } = player.config;
|
||||
Object.defineProperty(player.media, 'muted', {
|
||||
get() {
|
||||
return muted;
|
||||
},
|
||||
set(input) {
|
||||
const toggle = is.boolean(input) ? input : muted;
|
||||
muted = toggle;
|
||||
instance[toggle ? 'mute' : 'unMute']();
|
||||
triggerEvent.call(player, player.media, 'volumechange');
|
||||
},
|
||||
});
|
||||
|
||||
// Source
|
||||
Object.defineProperty(player.media, 'currentSrc', {
|
||||
get() {
|
||||
return instance.getVideoUrl();
|
||||
},
|
||||
});
|
||||
|
||||
// Ended
|
||||
Object.defineProperty(player.media, 'ended', {
|
||||
get() {
|
||||
return player.currentTime === player.duration;
|
||||
},
|
||||
});
|
||||
|
||||
// Get available speeds
|
||||
const speeds = instance.getAvailablePlaybackRates();
|
||||
// Filter based on config
|
||||
player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));
|
||||
|
||||
// Set the tabindex to avoid focus entering iframe
|
||||
if (player.supported.ui) {
|
||||
player.media.setAttribute('tabindex', -1);
|
||||
}
|
||||
|
||||
triggerEvent.call(player, player.media, 'timeupdate');
|
||||
triggerEvent.call(player, player.media, 'durationchange');
|
||||
|
||||
// Reset timer
|
||||
clearInterval(player.timers.buffering);
|
||||
|
||||
// Setup buffering
|
||||
player.timers.buffering = setInterval(() => {
|
||||
// Get loaded % from YouTube
|
||||
player.media.buffered = instance.getVideoLoadedFraction();
|
||||
|
||||
// Trigger progress only when we actually buffer something
|
||||
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
|
||||
triggerEvent.call(player, player.media, 'progress');
|
||||
}
|
||||
|
||||
// Set last buffer point
|
||||
player.media.lastBuffered = player.media.buffered;
|
||||
|
||||
// Bail if we're at 100%
|
||||
if (player.media.buffered === 1) {
|
||||
clearInterval(player.timers.buffering);
|
||||
|
||||
// Trigger event
|
||||
triggerEvent.call(player, player.media, 'canplaythrough');
|
||||
}
|
||||
}, 200);
|
||||
|
||||
// Rebuild UI
|
||||
setTimeout(() => ui.build.call(player), 50);
|
||||
},
|
||||
onStateChange(event) {
|
||||
// Get the instance
|
||||
const instance = event.target;
|
||||
|
||||
// Reset timer
|
||||
clearInterval(player.timers.playing);
|
||||
|
||||
const seeked = player.media.seeking && [1, 2].includes(event.data);
|
||||
|
||||
if (seeked) {
|
||||
// Unset seeking and fire seeked event
|
||||
player.media.seeking = false;
|
||||
triggerEvent.call(player, player.media, 'seeked');
|
||||
}
|
||||
|
||||
// Handle events
|
||||
// -1 Unstarted
|
||||
// 0 Ended
|
||||
// 1 Playing
|
||||
// 2 Paused
|
||||
// 3 Buffering
|
||||
// 5 Video cued
|
||||
switch (event.data) {
|
||||
case -1:
|
||||
// Update scrubber
|
||||
triggerEvent.call(player, player.media, 'timeupdate');
|
||||
|
||||
// Get loaded % from YouTube
|
||||
player.media.buffered = instance.getVideoLoadedFraction();
|
||||
triggerEvent.call(player, player.media, 'progress');
|
||||
|
||||
break;
|
||||
|
||||
case 0:
|
||||
assurePlaybackState.call(player, false);
|
||||
|
||||
// YouTube doesn't support loop for a single video, so mimick it.
|
||||
if (player.media.loop) {
|
||||
// YouTube needs a call to `stopVideo` before playing again
|
||||
instance.stopVideo();
|
||||
instance.playVideo();
|
||||
} else {
|
||||
triggerEvent.call(player, player.media, 'ended');
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
|
||||
if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
|
||||
player.media.pause();
|
||||
} else {
|
||||
assurePlaybackState.call(player, true);
|
||||
|
||||
triggerEvent.call(player, player.media, 'playing');
|
||||
|
||||
// Poll to get playback progress
|
||||
player.timers.playing = setInterval(() => {
|
||||
triggerEvent.call(player, player.media, 'timeupdate');
|
||||
}, 50);
|
||||
|
||||
// Check duration again due to YouTube bug
|
||||
// https://github.com/sampotts/plyr/issues/374
|
||||
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
|
||||
if (player.media.duration !== instance.getDuration()) {
|
||||
player.media.duration = instance.getDuration();
|
||||
triggerEvent.call(player, player.media, 'durationchange');
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
|
||||
if (!player.muted) {
|
||||
player.embed.unMute();
|
||||
}
|
||||
assurePlaybackState.call(player, false);
|
||||
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// Trigger waiting event to add loading classes to container as the video buffers.
|
||||
triggerEvent.call(player, player.media, 'waiting');
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
triggerEvent.call(player, player.elements.container, 'statechange', false, {
|
||||
code: event.data,
|
||||
});
|
||||
},
|
||||
// Seeking
|
||||
player.media.currentTime = 0;
|
||||
Object.defineProperty(player.media, 'currentTime', {
|
||||
get() {
|
||||
return Number(instance.getCurrentTime());
|
||||
},
|
||||
});
|
||||
},
|
||||
set(time) {
|
||||
// If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
|
||||
if (player.paused && !player.embed.hasPlayed) {
|
||||
player.embed.mute();
|
||||
}
|
||||
|
||||
// Set seeking state and trigger event
|
||||
player.media.seeking = true;
|
||||
triggerEvent.call(player, player.media, 'seeking');
|
||||
|
||||
// Seek after events sent
|
||||
instance.seekTo(time);
|
||||
},
|
||||
});
|
||||
|
||||
// Playback speed
|
||||
Object.defineProperty(player.media, 'playbackRate', {
|
||||
get() {
|
||||
return instance.getPlaybackRate();
|
||||
},
|
||||
set(input) {
|
||||
instance.setPlaybackRate(input);
|
||||
},
|
||||
});
|
||||
|
||||
// Volume
|
||||
let { volume } = player.config;
|
||||
Object.defineProperty(player.media, 'volume', {
|
||||
get() {
|
||||
return volume;
|
||||
},
|
||||
set(input) {
|
||||
volume = input;
|
||||
instance.setVolume(volume * 100);
|
||||
triggerEvent.call(player, player.media, 'volumechange');
|
||||
},
|
||||
});
|
||||
|
||||
// Muted
|
||||
let { muted } = player.config;
|
||||
Object.defineProperty(player.media, 'muted', {
|
||||
get() {
|
||||
return muted;
|
||||
},
|
||||
set(input) {
|
||||
const toggle = is.boolean(input) ? input : muted;
|
||||
muted = toggle;
|
||||
instance[toggle ? 'mute' : 'unMute']();
|
||||
instance.setVolume(volume * 100);
|
||||
triggerEvent.call(player, player.media, 'volumechange');
|
||||
},
|
||||
});
|
||||
|
||||
// Source
|
||||
Object.defineProperty(player.media, 'currentSrc', {
|
||||
get() {
|
||||
return instance.getVideoUrl();
|
||||
},
|
||||
});
|
||||
|
||||
// Ended
|
||||
Object.defineProperty(player.media, 'ended', {
|
||||
get() {
|
||||
return player.currentTime === player.duration;
|
||||
},
|
||||
});
|
||||
|
||||
// Get available speeds
|
||||
const speeds = instance.getAvailablePlaybackRates();
|
||||
// Filter based on config
|
||||
player.options.speed = speeds.filter((s) => player.config.speed.options.includes(s));
|
||||
|
||||
// Set the tabindex to avoid focus entering iframe
|
||||
if (player.supported.ui && config.customControls) {
|
||||
player.media.setAttribute('tabindex', -1);
|
||||
}
|
||||
|
||||
triggerEvent.call(player, player.media, 'timeupdate');
|
||||
triggerEvent.call(player, player.media, 'durationchange');
|
||||
|
||||
// Reset timer
|
||||
clearInterval(player.timers.buffering);
|
||||
|
||||
// Setup buffering
|
||||
player.timers.buffering = setInterval(() => {
|
||||
// Get loaded % from YouTube
|
||||
player.media.buffered = instance.getVideoLoadedFraction();
|
||||
|
||||
// Trigger progress only when we actually buffer something
|
||||
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
|
||||
triggerEvent.call(player, player.media, 'progress');
|
||||
}
|
||||
|
||||
// Set last buffer point
|
||||
player.media.lastBuffered = player.media.buffered;
|
||||
|
||||
// Bail if we're at 100%
|
||||
if (player.media.buffered === 1) {
|
||||
clearInterval(player.timers.buffering);
|
||||
|
||||
// Trigger event
|
||||
triggerEvent.call(player, player.media, 'canplaythrough');
|
||||
}
|
||||
}, 200);
|
||||
|
||||
// Rebuild UI
|
||||
if (config.customControls) {
|
||||
setTimeout(() => ui.build.call(player), 50);
|
||||
}
|
||||
},
|
||||
onStateChange(event) {
|
||||
// Get the instance
|
||||
const instance = event.target;
|
||||
|
||||
// Reset timer
|
||||
clearInterval(player.timers.playing);
|
||||
|
||||
const seeked = player.media.seeking && [1, 2].includes(event.data);
|
||||
|
||||
if (seeked) {
|
||||
// Unset seeking and fire seeked event
|
||||
player.media.seeking = false;
|
||||
triggerEvent.call(player, player.media, 'seeked');
|
||||
}
|
||||
|
||||
// Handle events
|
||||
// -1 Unstarted
|
||||
// 0 Ended
|
||||
// 1 Playing
|
||||
// 2 Paused
|
||||
// 3 Buffering
|
||||
// 5 Video cued
|
||||
switch (event.data) {
|
||||
case -1:
|
||||
// Update scrubber
|
||||
triggerEvent.call(player, player.media, 'timeupdate');
|
||||
|
||||
// Get loaded % from YouTube
|
||||
player.media.buffered = instance.getVideoLoadedFraction();
|
||||
triggerEvent.call(player, player.media, 'progress');
|
||||
|
||||
break;
|
||||
|
||||
case 0:
|
||||
assurePlaybackState.call(player, false);
|
||||
|
||||
// YouTube doesn't support loop for a single video, so mimick it.
|
||||
if (player.media.loop) {
|
||||
// YouTube needs a call to `stopVideo` before playing again
|
||||
instance.stopVideo();
|
||||
instance.playVideo();
|
||||
} else {
|
||||
triggerEvent.call(player, player.media, 'ended');
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
|
||||
if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
|
||||
player.media.pause();
|
||||
} else {
|
||||
assurePlaybackState.call(player, true);
|
||||
|
||||
triggerEvent.call(player, player.media, 'playing');
|
||||
|
||||
// Poll to get playback progress
|
||||
player.timers.playing = setInterval(() => {
|
||||
triggerEvent.call(player, player.media, 'timeupdate');
|
||||
}, 50);
|
||||
|
||||
// Check duration again due to YouTube bug
|
||||
// https://github.com/sampotts/plyr/issues/374
|
||||
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
|
||||
if (player.media.duration !== instance.getDuration()) {
|
||||
player.media.duration = instance.getDuration();
|
||||
triggerEvent.call(player, player.media, 'durationchange');
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
|
||||
if (!player.muted) {
|
||||
player.embed.unMute();
|
||||
}
|
||||
assurePlaybackState.call(player, false);
|
||||
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// Trigger waiting event to add loading classes to container as the video buffers.
|
||||
triggerEvent.call(player, player.media, 'waiting');
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
triggerEvent.call(player, player.elements.container, 'statechange', false, {
|
||||
code: event.data,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default youtube;
|
||||
|
||||
1082
src/js/plyr.d.ts
vendored
1082
src/js/plyr.d.ts
vendored
File diff suppressed because it is too large
Load Diff
2144
src/js/plyr.js
2144
src/js/plyr.js
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
// ==========================================================================
|
||||
// Plyr Polyfilled Build
|
||||
// plyr.js v3.5.10
|
||||
// plyr.js v3.7.0
|
||||
// https://github.com/sampotts/plyr
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
|
||||
244
src/js/source.js
244
src/js/source.js
@@ -13,146 +13,146 @@ import is from './utils/is';
|
||||
import { getDeep } from './utils/objects';
|
||||
|
||||
const source = {
|
||||
// Add elements to HTML5 media (source, tracks, etc)
|
||||
insertElements(type, attributes) {
|
||||
if (is.string(attributes)) {
|
||||
insertElement(type, this.media, {
|
||||
src: attributes,
|
||||
});
|
||||
} else if (is.array(attributes)) {
|
||||
attributes.forEach(attribute => {
|
||||
insertElement(type, this.media, attribute);
|
||||
});
|
||||
}
|
||||
},
|
||||
// Add elements to HTML5 media (source, tracks, etc)
|
||||
insertElements(type, attributes) {
|
||||
if (is.string(attributes)) {
|
||||
insertElement(type, this.media, {
|
||||
src: attributes,
|
||||
});
|
||||
} else if (is.array(attributes)) {
|
||||
attributes.forEach((attribute) => {
|
||||
insertElement(type, this.media, attribute);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Update source
|
||||
// Sources are not checked for support so be careful
|
||||
change(input) {
|
||||
if (!getDeep(input, 'sources.length')) {
|
||||
this.debug.warn('Invalid source format');
|
||||
return;
|
||||
// Update source
|
||||
// Sources are not checked for support so be careful
|
||||
change(input) {
|
||||
if (!getDeep(input, 'sources.length')) {
|
||||
this.debug.warn('Invalid source format');
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel current network requests
|
||||
html5.cancelRequests.call(this);
|
||||
|
||||
// Destroy instance and re-setup
|
||||
this.destroy.call(
|
||||
this,
|
||||
() => {
|
||||
// Reset quality options
|
||||
this.options.quality = [];
|
||||
|
||||
// Remove elements
|
||||
removeElement(this.media);
|
||||
this.media = null;
|
||||
|
||||
// Reset class name
|
||||
if (is.element(this.elements.container)) {
|
||||
this.elements.container.removeAttribute('class');
|
||||
}
|
||||
|
||||
// Cancel current network requests
|
||||
html5.cancelRequests.call(this);
|
||||
// Set the type and provider
|
||||
const { sources, type } = input;
|
||||
const [{ provider = providers.html5, src }] = sources;
|
||||
const tagName = provider === 'html5' ? type : 'div';
|
||||
const attributes = provider === 'html5' ? {} : { src };
|
||||
|
||||
// Destroy instance and re-setup
|
||||
this.destroy.call(
|
||||
this,
|
||||
() => {
|
||||
// Reset quality options
|
||||
this.options.quality = [];
|
||||
Object.assign(this, {
|
||||
provider,
|
||||
type,
|
||||
// Check for support
|
||||
supported: support.check(type, provider, this.config.playsinline),
|
||||
// Create new element
|
||||
media: createElement(tagName, attributes),
|
||||
});
|
||||
|
||||
// Remove elements
|
||||
removeElement(this.media);
|
||||
this.media = null;
|
||||
// Inject the new element
|
||||
this.elements.container.appendChild(this.media);
|
||||
|
||||
// Reset class name
|
||||
if (is.element(this.elements.container)) {
|
||||
this.elements.container.removeAttribute('class');
|
||||
}
|
||||
// Autoplay the new source?
|
||||
if (is.boolean(input.autoplay)) {
|
||||
this.config.autoplay = input.autoplay;
|
||||
}
|
||||
|
||||
// Set the type and provider
|
||||
const { sources, type } = input;
|
||||
const [{ provider = providers.html5, src }] = sources;
|
||||
const tagName = provider === 'html5' ? type : 'div';
|
||||
const attributes = provider === 'html5' ? {} : { src };
|
||||
// Set attributes for audio and video
|
||||
if (this.isHTML5) {
|
||||
if (this.config.crossorigin) {
|
||||
this.media.setAttribute('crossorigin', '');
|
||||
}
|
||||
if (this.config.autoplay) {
|
||||
this.media.setAttribute('autoplay', '');
|
||||
}
|
||||
if (!is.empty(input.poster)) {
|
||||
this.poster = input.poster;
|
||||
}
|
||||
if (this.config.loop.active) {
|
||||
this.media.setAttribute('loop', '');
|
||||
}
|
||||
if (this.config.muted) {
|
||||
this.media.setAttribute('muted', '');
|
||||
}
|
||||
if (this.config.playsinline) {
|
||||
this.media.setAttribute('playsinline', '');
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(this, {
|
||||
provider,
|
||||
type,
|
||||
// Check for support
|
||||
supported: support.check(type, provider, this.config.playsinline),
|
||||
// Create new element
|
||||
media: createElement(tagName, attributes),
|
||||
});
|
||||
// Restore class hook
|
||||
ui.addStyleHook.call(this);
|
||||
|
||||
// Inject the new element
|
||||
this.elements.container.appendChild(this.media);
|
||||
// Set new sources for html5
|
||||
if (this.isHTML5) {
|
||||
source.insertElements.call(this, 'source', sources);
|
||||
}
|
||||
|
||||
// Autoplay the new source?
|
||||
if (is.boolean(input.autoplay)) {
|
||||
this.config.autoplay = input.autoplay;
|
||||
}
|
||||
// Set video title
|
||||
this.config.title = input.title;
|
||||
|
||||
// Set attributes for audio and video
|
||||
if (this.isHTML5) {
|
||||
if (this.config.crossorigin) {
|
||||
this.media.setAttribute('crossorigin', '');
|
||||
}
|
||||
if (this.config.autoplay) {
|
||||
this.media.setAttribute('autoplay', '');
|
||||
}
|
||||
if (!is.empty(input.poster)) {
|
||||
this.poster = input.poster;
|
||||
}
|
||||
if (this.config.loop.active) {
|
||||
this.media.setAttribute('loop', '');
|
||||
}
|
||||
if (this.config.muted) {
|
||||
this.media.setAttribute('muted', '');
|
||||
}
|
||||
if (this.config.playsinline) {
|
||||
this.media.setAttribute('playsinline', '');
|
||||
}
|
||||
}
|
||||
// Set up from scratch
|
||||
media.setup.call(this);
|
||||
|
||||
// Restore class hook
|
||||
ui.addStyleHook.call(this);
|
||||
// HTML5 stuff
|
||||
if (this.isHTML5) {
|
||||
// Setup captions
|
||||
if (Object.keys(input).includes('tracks')) {
|
||||
source.insertElements.call(this, 'track', input.tracks);
|
||||
}
|
||||
}
|
||||
|
||||
// Set new sources for html5
|
||||
if (this.isHTML5) {
|
||||
source.insertElements.call(this, 'source', sources);
|
||||
}
|
||||
// If HTML5 or embed but not fully supported, setupInterface and call ready now
|
||||
if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {
|
||||
// Setup interface
|
||||
ui.build.call(this);
|
||||
}
|
||||
|
||||
// Set video title
|
||||
this.config.title = input.title;
|
||||
// Load HTML5 sources
|
||||
if (this.isHTML5) {
|
||||
this.media.load();
|
||||
}
|
||||
|
||||
// Set up from scratch
|
||||
media.setup.call(this);
|
||||
// Update previewThumbnails config & reload plugin
|
||||
if (!is.empty(input.previewThumbnails)) {
|
||||
Object.assign(this.config.previewThumbnails, input.previewThumbnails);
|
||||
|
||||
// HTML5 stuff
|
||||
if (this.isHTML5) {
|
||||
// Setup captions
|
||||
if (Object.keys(input).includes('tracks')) {
|
||||
source.insertElements.call(this, 'track', input.tracks);
|
||||
}
|
||||
}
|
||||
// Cleanup previewThumbnails plugin if it was loaded
|
||||
if (this.previewThumbnails && this.previewThumbnails.loaded) {
|
||||
this.previewThumbnails.destroy();
|
||||
this.previewThumbnails = null;
|
||||
}
|
||||
|
||||
// If HTML5 or embed but not fully supported, setupInterface and call ready now
|
||||
if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {
|
||||
// Setup interface
|
||||
ui.build.call(this);
|
||||
}
|
||||
// Create new instance if it is still enabled
|
||||
if (this.config.previewThumbnails.enabled) {
|
||||
this.previewThumbnails = new PreviewThumbnails(this);
|
||||
}
|
||||
}
|
||||
|
||||
// Load HTML5 sources
|
||||
if (this.isHTML5) {
|
||||
this.media.load();
|
||||
}
|
||||
|
||||
// Update previewThumbnails config & reload plugin
|
||||
if (!is.empty(input.previewThumbnails)) {
|
||||
Object.assign(this.config.previewThumbnails, input.previewThumbnails);
|
||||
|
||||
// Cleanup previewThumbnails plugin if it was loaded
|
||||
if (this.previewThumbnails && this.previewThumbnails.loaded) {
|
||||
this.previewThumbnails.destroy();
|
||||
this.previewThumbnails = null;
|
||||
}
|
||||
|
||||
// Create new instance if it is still enabled
|
||||
if (this.config.previewThumbnails.enabled) {
|
||||
this.previewThumbnails = new PreviewThumbnails(this);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the fullscreen support
|
||||
this.fullscreen.update();
|
||||
},
|
||||
true,
|
||||
);
|
||||
},
|
||||
// Update the fullscreen support
|
||||
this.fullscreen.update();
|
||||
},
|
||||
true,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default source;
|
||||
|
||||
@@ -6,72 +6,76 @@ import is from './utils/is';
|
||||
import { extend } from './utils/objects';
|
||||
|
||||
class Storage {
|
||||
constructor(player) {
|
||||
this.enabled = player.config.storage.enabled;
|
||||
this.key = player.config.storage.key;
|
||||
constructor(player) {
|
||||
this.enabled = player.config.storage.enabled;
|
||||
this.key = player.config.storage.key;
|
||||
}
|
||||
|
||||
// Check for actual support (see if we can use it)
|
||||
static get supported() {
|
||||
try {
|
||||
if (!('localStorage' in window)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const test = '___test';
|
||||
|
||||
// Try to use it (it might be disabled, e.g. user is in private mode)
|
||||
// see: https://github.com/sampotts/plyr/issues/131
|
||||
window.localStorage.setItem(test, test);
|
||||
window.localStorage.removeItem(test);
|
||||
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
get = (key) => {
|
||||
if (!Storage.supported || !this.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for actual support (see if we can use it)
|
||||
static get supported() {
|
||||
try {
|
||||
if (!('localStorage' in window)) {
|
||||
return false;
|
||||
}
|
||||
const store = window.localStorage.getItem(this.key);
|
||||
|
||||
const test = '___test';
|
||||
|
||||
// Try to use it (it might be disabled, e.g. user is in private mode)
|
||||
// see: https://github.com/sampotts/plyr/issues/131
|
||||
window.localStorage.setItem(test, test);
|
||||
window.localStorage.removeItem(test);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
if (is.empty(store)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
get(key) {
|
||||
if (!Storage.supported || !this.enabled) {
|
||||
return null;
|
||||
}
|
||||
const json = JSON.parse(store);
|
||||
|
||||
const store = window.localStorage.getItem(this.key);
|
||||
return is.string(key) && key.length ? json[key] : json;
|
||||
};
|
||||
|
||||
if (is.empty(store)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const json = JSON.parse(store);
|
||||
|
||||
return is.string(key) && key.length ? json[key] : json;
|
||||
set = (object) => {
|
||||
// Bail if we don't have localStorage support or it's disabled
|
||||
if (!Storage.supported || !this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(object) {
|
||||
// Bail if we don't have localStorage support or it's disabled
|
||||
if (!Storage.supported || !this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Can only store objectst
|
||||
if (!is.object(object)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current storage
|
||||
let storage = this.get();
|
||||
|
||||
// Default to empty object
|
||||
if (is.empty(storage)) {
|
||||
storage = {};
|
||||
}
|
||||
|
||||
// Update the working copy of the values
|
||||
extend(storage, object);
|
||||
|
||||
// Update storage
|
||||
window.localStorage.setItem(this.key, JSON.stringify(storage));
|
||||
// Can only store objectst
|
||||
if (!is.object(object)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current storage
|
||||
let storage = this.get();
|
||||
|
||||
// Default to empty object
|
||||
if (is.empty(storage)) {
|
||||
storage = {};
|
||||
}
|
||||
|
||||
// Update the working copy of the values
|
||||
extend(storage, object);
|
||||
|
||||
// Update storage
|
||||
try {
|
||||
window.localStorage.setItem(this.key, JSON.stringify(storage));
|
||||
} catch (_) {
|
||||
// Do nothing
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Storage;
|
||||
|
||||
@@ -9,110 +9,110 @@ 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',
|
||||
'audio/ogg': 'vorbis',
|
||||
'audio/wav': '1',
|
||||
'video/webm': 'vp8, vorbis',
|
||||
'video/mp4': 'avc1.42E01E, mp4a.40.2',
|
||||
'video/ogg': 'theora',
|
||||
};
|
||||
|
||||
// Check for feature support
|
||||
const support = {
|
||||
// Basic support
|
||||
audio: 'canPlayType' in document.createElement('audio'),
|
||||
video: 'canPlayType' in document.createElement('video'),
|
||||
// Basic support
|
||||
audio: 'canPlayType' in document.createElement('audio'),
|
||||
video: 'canPlayType' in document.createElement('video'),
|
||||
|
||||
// Check for support
|
||||
// Basic functionality vs full UI
|
||||
check(type, provider, playsinline) {
|
||||
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
|
||||
const api = support[type] || provider !== 'html5';
|
||||
const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
|
||||
// Check for support
|
||||
// Basic functionality vs full UI
|
||||
check(type, provider, playsinline) {
|
||||
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
|
||||
const api = support[type] || provider !== 'html5';
|
||||
const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
|
||||
|
||||
return {
|
||||
api,
|
||||
ui,
|
||||
};
|
||||
},
|
||||
return {
|
||||
api,
|
||||
ui,
|
||||
};
|
||||
},
|
||||
|
||||
// Picture-in-picture support
|
||||
// Safari & Chrome only currently
|
||||
pip: (() => {
|
||||
if (browser.isIPhone) {
|
||||
return false;
|
||||
}
|
||||
// Picture-in-picture support
|
||||
// Safari & Chrome only currently
|
||||
pip: (() => {
|
||||
if (browser.isIPhone) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Safari
|
||||
// https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls
|
||||
if (is.function(createElement('video').webkitSetPresentationMode)) {
|
||||
return true;
|
||||
}
|
||||
// Safari
|
||||
// https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls
|
||||
if (is.function(createElement('video').webkitSetPresentationMode)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Chrome
|
||||
// https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture
|
||||
if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {
|
||||
return true;
|
||||
}
|
||||
// Chrome
|
||||
// https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture
|
||||
if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})(),
|
||||
return false;
|
||||
})(),
|
||||
|
||||
// Airplay support
|
||||
// Safari only currently
|
||||
airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
|
||||
// Airplay support
|
||||
// Safari only currently
|
||||
airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
|
||||
|
||||
// Inline playback support
|
||||
// https://webkit.org/blog/6784/new-video-policies-for-ios/
|
||||
playsinline: 'playsInline' in document.createElement('video'),
|
||||
// Inline playback support
|
||||
// https://webkit.org/blog/6784/new-video-policies-for-ios/
|
||||
playsinline: 'playsInline' in document.createElement('video'),
|
||||
|
||||
// Check for mime type support against a player instance
|
||||
// Credits: http://diveintohtml5.info/everything.html
|
||||
// Related: http://www.leanbackplayer.com/test/h5mt.html
|
||||
mime(input) {
|
||||
if (is.empty(input)) {
|
||||
return false;
|
||||
}
|
||||
// Check for mime type support against a player instance
|
||||
// Credits: http://diveintohtml5.info/everything.html
|
||||
// Related: http://www.leanbackplayer.com/test/h5mt.html
|
||||
mime(input) {
|
||||
if (is.empty(input)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [mediaType] = input.split('/');
|
||||
let type = input;
|
||||
const [mediaType] = input.split('/');
|
||||
let type = input;
|
||||
|
||||
// Verify we're using HTML5 and there's no media type mismatch
|
||||
if (!this.isHTML5 || mediaType !== this.type) {
|
||||
return false;
|
||||
}
|
||||
// Verify we're using HTML5 and there's no media type mismatch
|
||||
if (!this.isHTML5 || mediaType !== this.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add codec if required
|
||||
if (Object.keys(defaultCodecs).includes(type)) {
|
||||
type += `; codecs="${defaultCodecs[input]}"`;
|
||||
}
|
||||
// Add codec if required
|
||||
if (Object.keys(defaultCodecs).includes(type)) {
|
||||
type += `; codecs="${defaultCodecs[input]}"`;
|
||||
}
|
||||
|
||||
try {
|
||||
return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
try {
|
||||
return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Check for textTracks support
|
||||
textTracks: 'textTracks' in document.createElement('video'),
|
||||
// Check for textTracks support
|
||||
textTracks: 'textTracks' in document.createElement('video'),
|
||||
|
||||
// <input type="range"> Sliders
|
||||
rangeInput: (() => {
|
||||
const range = document.createElement('input');
|
||||
range.type = 'range';
|
||||
return range.type === 'range';
|
||||
})(),
|
||||
// <input type="range"> Sliders
|
||||
rangeInput: (() => {
|
||||
const range = document.createElement('input');
|
||||
range.type = 'range';
|
||||
return range.type === 'range';
|
||||
})(),
|
||||
|
||||
// Touch
|
||||
// NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
|
||||
touch: 'ontouchstart' in document.documentElement,
|
||||
// Touch
|
||||
// NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
|
||||
touch: 'ontouchstart' in document.documentElement,
|
||||
|
||||
// Detect transitions support
|
||||
transitions: transitionEndEvent !== false,
|
||||
// Detect transitions support
|
||||
transitions: transitionEndEvent !== false,
|
||||
|
||||
// Reduced motion iOS & MacOS setting
|
||||
// https://webkit.org/blog/7551/responsive-design-for-motion/
|
||||
reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,
|
||||
// Reduced motion iOS & MacOS setting
|
||||
// https://webkit.org/blog/7551/responsive-design-for-motion/
|
||||
reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,
|
||||
};
|
||||
|
||||
export default support;
|
||||
|
||||
441
src/js/ui.js
441
src/js/ui.js
@@ -13,267 +13,288 @@ import is from './utils/is';
|
||||
import loadImage from './utils/load-image';
|
||||
|
||||
const ui = {
|
||||
addStyleHook() {
|
||||
toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
|
||||
toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
|
||||
},
|
||||
addStyleHook() {
|
||||
toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
|
||||
toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
|
||||
},
|
||||
|
||||
// Toggle native HTML5 media controls
|
||||
toggleNativeControls(toggle = false) {
|
||||
if (toggle && this.isHTML5) {
|
||||
this.media.setAttribute('controls', '');
|
||||
} else {
|
||||
this.media.removeAttribute('controls');
|
||||
}
|
||||
},
|
||||
// Toggle native HTML5 media controls
|
||||
toggleNativeControls(toggle = false) {
|
||||
if (toggle && this.isHTML5) {
|
||||
this.media.setAttribute('controls', '');
|
||||
} else {
|
||||
this.media.removeAttribute('controls');
|
||||
}
|
||||
},
|
||||
|
||||
// Setup the UI
|
||||
build() {
|
||||
// Re-attach media element listeners
|
||||
// TODO: Use event bubbling?
|
||||
this.listeners.media();
|
||||
// Setup the UI
|
||||
build() {
|
||||
// Re-attach media element listeners
|
||||
// TODO: Use event bubbling?
|
||||
this.listeners.media();
|
||||
|
||||
// Don't setup interface if no support
|
||||
if (!this.supported.ui) {
|
||||
this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
|
||||
// Don't setup interface if no support
|
||||
if (!this.supported.ui) {
|
||||
this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
|
||||
|
||||
// Restore native controls
|
||||
ui.toggleNativeControls.call(this, true);
|
||||
// Restore native controls
|
||||
ui.toggleNativeControls.call(this, true);
|
||||
|
||||
// Bail
|
||||
return;
|
||||
}
|
||||
// Bail
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject custom controls if not present
|
||||
if (!is.element(this.elements.controls)) {
|
||||
// Inject custom controls
|
||||
controls.inject.call(this);
|
||||
// Inject custom controls if not present
|
||||
if (!is.element(this.elements.controls)) {
|
||||
// Inject custom controls
|
||||
controls.inject.call(this);
|
||||
|
||||
// Re-attach control listeners
|
||||
this.listeners.controls();
|
||||
}
|
||||
// Re-attach control listeners
|
||||
this.listeners.controls();
|
||||
}
|
||||
|
||||
// Remove native controls
|
||||
ui.toggleNativeControls.call(this);
|
||||
// Remove native controls
|
||||
ui.toggleNativeControls.call(this);
|
||||
|
||||
// Setup captions for HTML5
|
||||
if (this.isHTML5) {
|
||||
captions.setup.call(this);
|
||||
}
|
||||
// Setup captions for HTML5
|
||||
if (this.isHTML5) {
|
||||
captions.setup.call(this);
|
||||
}
|
||||
|
||||
// Reset volume
|
||||
this.volume = null;
|
||||
// Reset volume
|
||||
this.volume = null;
|
||||
|
||||
// Reset mute state
|
||||
this.muted = null;
|
||||
// Reset mute state
|
||||
this.muted = null;
|
||||
|
||||
// Reset loop state
|
||||
this.loop = null;
|
||||
// Reset loop state
|
||||
this.loop = null;
|
||||
|
||||
// Reset quality setting
|
||||
this.quality = null;
|
||||
// Reset quality setting
|
||||
this.quality = null;
|
||||
|
||||
// Reset speed
|
||||
this.speed = null;
|
||||
// Reset speed
|
||||
this.speed = null;
|
||||
|
||||
// Reset volume display
|
||||
controls.updateVolume.call(this);
|
||||
// Reset volume display
|
||||
controls.updateVolume.call(this);
|
||||
|
||||
// Reset time display
|
||||
controls.timeUpdate.call(this);
|
||||
// Reset time display
|
||||
controls.timeUpdate.call(this);
|
||||
|
||||
// Update the UI
|
||||
ui.checkPlaying.call(this);
|
||||
// Reset duration display
|
||||
controls.durationUpdate.call(this);
|
||||
|
||||
// Check for picture-in-picture support
|
||||
toggleClass(
|
||||
this.elements.container,
|
||||
this.config.classNames.pip.supported,
|
||||
support.pip && this.isHTML5 && this.isVideo,
|
||||
);
|
||||
// Update the UI
|
||||
ui.checkPlaying.call(this);
|
||||
|
||||
// Check for airplay support
|
||||
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
||||
// Check for picture-in-picture support
|
||||
toggleClass(
|
||||
this.elements.container,
|
||||
this.config.classNames.pip.supported,
|
||||
support.pip && this.isHTML5 && this.isVideo,
|
||||
);
|
||||
|
||||
// Add iOS class
|
||||
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
||||
// Check for airplay support
|
||||
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
||||
|
||||
// Add touch class
|
||||
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
|
||||
// Add iOS class
|
||||
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
||||
|
||||
// Ready for API calls
|
||||
this.ready = true;
|
||||
// Add touch class
|
||||
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
|
||||
|
||||
// Ready event at end of execution stack
|
||||
setTimeout(() => {
|
||||
triggerEvent.call(this, this.media, 'ready');
|
||||
}, 0);
|
||||
// Ready for API calls
|
||||
this.ready = true;
|
||||
|
||||
// Set the title
|
||||
ui.setTitle.call(this);
|
||||
// Ready event at end of execution stack
|
||||
setTimeout(() => {
|
||||
triggerEvent.call(this, this.media, 'ready');
|
||||
}, 0);
|
||||
|
||||
// Assure the poster image is set, if the property was added before the element was created
|
||||
if (this.poster) {
|
||||
ui.setPoster.call(this, this.poster, false).catch(() => {});
|
||||
}
|
||||
// Set the title
|
||||
ui.setTitle.call(this);
|
||||
|
||||
// Manually set the duration if user has overridden it.
|
||||
// The event listeners for it doesn't get called if preload is disabled (#701)
|
||||
if (this.config.duration) {
|
||||
controls.durationUpdate.call(this);
|
||||
}
|
||||
},
|
||||
// Assure the poster image is set, if the property was added before the element was created
|
||||
if (this.poster) {
|
||||
ui.setPoster.call(this, this.poster, false).catch(() => {});
|
||||
}
|
||||
|
||||
// Setup aria attribute for play and iframe title
|
||||
setTitle() {
|
||||
// Find the current text
|
||||
let label = i18n.get('play', this.config);
|
||||
// Manually set the duration if user has overridden it.
|
||||
// The event listeners for it doesn't get called if preload is disabled (#701)
|
||||
if (this.config.duration) {
|
||||
controls.durationUpdate.call(this);
|
||||
}
|
||||
|
||||
// If there's a media title set, use that for the label
|
||||
if (is.string(this.config.title) && !is.empty(this.config.title)) {
|
||||
label += `, ${this.config.title}`;
|
||||
}
|
||||
// Media metadata
|
||||
if (this.config.mediaMetadata) {
|
||||
controls.setMediaMetadata.call(this);
|
||||
}
|
||||
},
|
||||
|
||||
// If there's a play button, set label
|
||||
Array.from(this.elements.buttons.play || []).forEach(button => {
|
||||
button.setAttribute('aria-label', label);
|
||||
});
|
||||
// Setup aria attribute for play and iframe title
|
||||
setTitle() {
|
||||
// Find the current text
|
||||
let label = i18n.get('play', this.config);
|
||||
|
||||
// Set iframe title
|
||||
// https://github.com/sampotts/plyr/issues/124
|
||||
if (this.isEmbed) {
|
||||
const iframe = getElement.call(this, 'iframe');
|
||||
// If there's a media title set, use that for the label
|
||||
if (is.string(this.config.title) && !is.empty(this.config.title)) {
|
||||
label += `, ${this.config.title}`;
|
||||
}
|
||||
|
||||
if (!is.element(iframe)) {
|
||||
return;
|
||||
}
|
||||
// If there's a play button, set label
|
||||
Array.from(this.elements.buttons.play || []).forEach((button) => {
|
||||
button.setAttribute('aria-label', label);
|
||||
});
|
||||
|
||||
// Default to media type
|
||||
const title = !is.empty(this.config.title) ? this.config.title : 'video';
|
||||
const format = i18n.get('frameTitle', this.config);
|
||||
// Set iframe title
|
||||
// https://github.com/sampotts/plyr/issues/124
|
||||
if (this.isEmbed) {
|
||||
const iframe = getElement.call(this, 'iframe');
|
||||
|
||||
iframe.setAttribute('title', format.replace('{title}', title));
|
||||
}
|
||||
},
|
||||
if (!is.element(iframe)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle poster
|
||||
togglePoster(enable) {
|
||||
toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
|
||||
},
|
||||
// Default to media type
|
||||
const title = !is.empty(this.config.title) ? this.config.title : 'video';
|
||||
const format = i18n.get('frameTitle', this.config);
|
||||
|
||||
// Set the poster image (async)
|
||||
// Used internally for the poster setter, with the passive option forced to false
|
||||
setPoster(poster, passive = true) {
|
||||
// Don't override if call is passive
|
||||
if (passive && this.poster) {
|
||||
return Promise.reject(new Error('Poster already set'));
|
||||
}
|
||||
iframe.setAttribute('title', format.replace('{title}', title));
|
||||
}
|
||||
},
|
||||
|
||||
// Set property synchronously to respect the call order
|
||||
this.media.setAttribute('poster', poster);
|
||||
// Toggle poster
|
||||
togglePoster(enable) {
|
||||
toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
|
||||
},
|
||||
|
||||
// HTML5 uses native poster attribute
|
||||
if (this.isHTML5) {
|
||||
return Promise.resolve(poster);
|
||||
}
|
||||
// Set the poster image (async)
|
||||
// Used internally for the poster setter, with the passive option forced to false
|
||||
setPoster(poster, passive = true) {
|
||||
// Don't override if call is passive
|
||||
if (passive && this.poster) {
|
||||
return Promise.reject(new Error('Poster already set'));
|
||||
}
|
||||
|
||||
// Wait until ui is ready
|
||||
return (
|
||||
ready
|
||||
.call(this)
|
||||
// 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: '',
|
||||
});
|
||||
// Set property synchronously to respect the call order
|
||||
this.media.setAttribute('data-poster', poster);
|
||||
|
||||
ui.togglePoster.call(this, true);
|
||||
// Show the poster
|
||||
this.elements.poster.removeAttribute('hidden');
|
||||
|
||||
return poster;
|
||||
})
|
||||
);
|
||||
},
|
||||
// Wait until ui is ready
|
||||
return (
|
||||
ready
|
||||
.call(this)
|
||||
// Load image
|
||||
.then(() => loadImage(poster))
|
||||
.catch((error) => {
|
||||
// Hide poster on error unless it's been set by another call
|
||||
if (poster === this.poster) {
|
||||
ui.togglePoster.call(this, false);
|
||||
}
|
||||
// Rethrow
|
||||
throw error;
|
||||
})
|
||||
.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: '',
|
||||
});
|
||||
|
||||
// Check playing state
|
||||
checkPlaying(event) {
|
||||
// Class hooks
|
||||
toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
|
||||
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
|
||||
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
|
||||
ui.togglePoster.call(this, true);
|
||||
|
||||
// Set state
|
||||
Array.from(this.elements.buttons.play || []).forEach(target => {
|
||||
Object.assign(target, { pressed: this.playing });
|
||||
target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
|
||||
});
|
||||
return poster;
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
// Only update controls on non timeupdate events
|
||||
if (is.event(event) && event.type === 'timeupdate') {
|
||||
return;
|
||||
}
|
||||
// Check playing state
|
||||
checkPlaying(event) {
|
||||
// Class hooks
|
||||
toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
|
||||
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
|
||||
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
|
||||
|
||||
// Toggle controls
|
||||
// Set state
|
||||
Array.from(this.elements.buttons.play || []).forEach((target) => {
|
||||
Object.assign(target, { pressed: this.playing });
|
||||
target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
|
||||
});
|
||||
|
||||
// Only update controls on non timeupdate events
|
||||
if (is.event(event) && event.type === 'timeupdate') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle controls
|
||||
ui.toggleControls.call(this);
|
||||
},
|
||||
|
||||
// Check if media is loading
|
||||
checkLoading(event) {
|
||||
this.loading = ['stalled', 'waiting'].includes(event.type);
|
||||
|
||||
// Clear timer
|
||||
clearTimeout(this.timers.loading);
|
||||
|
||||
// Timer to prevent flicker when seeking
|
||||
this.timers.loading = setTimeout(
|
||||
() => {
|
||||
// Update progress bar loading class state
|
||||
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||
|
||||
// Update controls visibility
|
||||
ui.toggleControls.call(this);
|
||||
},
|
||||
},
|
||||
this.loading ? 250 : 0,
|
||||
);
|
||||
},
|
||||
|
||||
// Check if media is loading
|
||||
checkLoading(event) {
|
||||
this.loading = ['stalled', 'waiting'].includes(event.type);
|
||||
// Toggle controls based on state and `force` argument
|
||||
toggleControls(force) {
|
||||
const { controls: controlsElement } = this.elements;
|
||||
|
||||
// Clear timer
|
||||
clearTimeout(this.timers.loading);
|
||||
if (controlsElement && this.config.hideControls) {
|
||||
// Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
|
||||
const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();
|
||||
|
||||
// Timer to prevent flicker when seeking
|
||||
this.timers.loading = setTimeout(
|
||||
() => {
|
||||
// Update progress bar loading class state
|
||||
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
|
||||
this.toggleControls(
|
||||
Boolean(
|
||||
force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// Update controls visibility
|
||||
ui.toggleControls.call(this);
|
||||
},
|
||||
this.loading ? 250 : 0,
|
||||
);
|
||||
},
|
||||
// Migrate any custom properties from the media to the parent
|
||||
migrateStyles() {
|
||||
// Loop through values (as they are the keys when the object is spread 🤔)
|
||||
Object.values({ ...this.media.style })
|
||||
// We're only fussed about Plyr specific properties
|
||||
.filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))
|
||||
.forEach((key) => {
|
||||
// Set on the container
|
||||
this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));
|
||||
|
||||
// Toggle controls based on state and `force` argument
|
||||
toggleControls(force) {
|
||||
const { controls: controlsElement } = this.elements;
|
||||
// Clean up from media element
|
||||
this.media.style.removeProperty(key);
|
||||
});
|
||||
|
||||
if (controlsElement && this.config.hideControls) {
|
||||
// Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
|
||||
const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();
|
||||
|
||||
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
|
||||
this.toggleControls(
|
||||
Boolean(
|
||||
force ||
|
||||
this.loading ||
|
||||
this.paused ||
|
||||
controlsElement.pressed ||
|
||||
controlsElement.hover ||
|
||||
recentTouchSeek,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
// Remove attribute if empty
|
||||
if (is.empty(this.media.style)) {
|
||||
this.media.removeAttribute('style');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default ui;
|
||||
|
||||
@@ -5,34 +5,34 @@
|
||||
import is from './is';
|
||||
|
||||
export const transitionEndEvent = (() => {
|
||||
const element = document.createElement('span');
|
||||
const element = document.createElement('span');
|
||||
|
||||
const events = {
|
||||
WebkitTransition: 'webkitTransitionEnd',
|
||||
MozTransition: 'transitionend',
|
||||
OTransition: 'oTransitionEnd otransitionend',
|
||||
transition: 'transitionend',
|
||||
};
|
||||
const events = {
|
||||
WebkitTransition: 'webkitTransitionEnd',
|
||||
MozTransition: 'transitionend',
|
||||
OTransition: 'oTransitionEnd otransitionend',
|
||||
transition: 'transitionend',
|
||||
};
|
||||
|
||||
const type = Object.keys(events).find(event => element.style[event] !== undefined);
|
||||
const type = Object.keys(events).find((event) => element.style[event] !== undefined);
|
||||
|
||||
return is.string(type) ? events[type] : false;
|
||||
return is.string(type) ? events[type] : false;
|
||||
})();
|
||||
|
||||
// Force repaint of element
|
||||
export function repaint(element, delay) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
element.hidden = true;
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
element.hidden = true;
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
element.offsetHeight;
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
element.offsetHeight;
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
element.hidden = false;
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
}, delay);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
element.hidden = false;
|
||||
} catch (_) {
|
||||
// Do nothing
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user