Compare commits
478 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 | ||
|
|
206e3b57d1 | ||
|
|
a34bc3ef29 | ||
|
|
6350b7b9e4 | ||
|
|
04d06f2242 | ||
|
|
924049aa14 | ||
|
|
442427ebd5 | ||
|
|
7954c92c0b | ||
|
|
5afb14283a | ||
|
|
bfc541b880 | ||
|
|
426280f90c | ||
|
|
2e2c5ad72a | ||
|
|
ecb091af6b | ||
|
|
ecb882b719 | ||
|
|
cddd9c30db | ||
|
|
f6a4625495 | ||
|
|
550bd543e3 | ||
|
|
a6ff0274a9 | ||
|
|
841746210a | ||
|
|
156abda66a | ||
|
|
1619510dcf | ||
|
|
ff8dedd4ec | ||
|
|
637823c8ce | ||
|
|
01219be817 | ||
|
|
59e3afba03 | ||
|
|
db05322ba2 | ||
|
|
74e5c78b3f | ||
|
|
ea350f9d1d | ||
|
|
eda192639d | ||
|
|
0f16a018ff | ||
|
|
7ca74f48bc | ||
|
|
5837c2d5f0 | ||
|
|
e50b35d195 | ||
|
|
433b729c45 | ||
|
|
bb7f7d5e2a | ||
|
|
8c44425665 | ||
|
|
93e3f8946a | ||
|
|
95431639a0 | ||
|
|
3e3186cfeb | ||
|
|
2d13ad3d39 | ||
|
|
d7d0f5fca0 | ||
|
|
74ba6a96fc | ||
|
|
e1cb2f24f5 | ||
|
|
59e3ef7248 | ||
|
|
0b1c480729 | ||
|
|
90dc985657 | ||
|
|
b5456e1de7 | ||
|
|
976eebc2a2 | ||
|
|
f00c279366 | ||
|
|
b651d6f027 | ||
|
|
b63b62f6dc | ||
|
|
0f08c7c13a | ||
|
|
97f8093a8d | ||
|
|
9075ea189a | ||
|
|
c33f0995f9 | ||
|
|
e17da7dfd4 | ||
|
|
299f712cc9 | ||
|
|
f755a3c401 | ||
|
|
472bb479d4 | ||
|
|
61a24eab76 | ||
|
|
8b9521d5a5 | ||
|
|
818e1efd43 | ||
|
|
58f5380694 | ||
|
|
9d51291125 | ||
|
|
fefcca7805 | ||
|
|
5204f33d45 | ||
|
|
0041ea3050 | ||
|
|
f8a28b632c | ||
|
|
9c817cff68 | ||
|
|
521e8abf88 | ||
|
|
9fe03c7474 | ||
|
|
c3f10e7df3 | ||
|
|
1b8c51f45e | ||
|
|
1bb4c207f1 | ||
|
|
b6da2702a2 | ||
|
|
fb704b945d | ||
|
|
71d6f59d56 | ||
|
|
89136bc2e6 | ||
|
|
b6d94e000f | ||
|
|
bfcb7133cb | ||
|
|
7883792ccc | ||
|
|
def3668030 | ||
|
|
a77d2d56f6 | ||
|
|
7ee041403f | ||
|
|
53a400027f | ||
|
|
a2498acf7c | ||
|
|
28a1098c0f | ||
|
|
6ffaef35cf | ||
|
|
ff105ee203 | ||
|
|
56c0d7bd4d | ||
|
|
d00d31961e | ||
|
|
b2d3ef5f38 | ||
|
|
b2ac730572 | ||
|
|
3424d08d3a | ||
|
|
5dd9462bed | ||
|
|
402eb2b761 | ||
|
|
63d74eee68 | ||
|
|
166a27d094 | ||
|
|
4f06e2eb71 | ||
|
|
0b240ae7d1 | ||
|
|
6b0e5cd6f1 | ||
|
|
2463434d27 | ||
|
|
c09b9ac01c | ||
|
|
15a1cdde89 | ||
|
|
3a1da5ad36 | ||
|
|
67cb324aed | ||
|
|
8736fa8a52 | ||
|
|
cb4dab4250 | ||
|
|
c56916a8e0 | ||
|
|
93963d3915 | ||
|
|
e7c6f965b4 | ||
|
|
4f263ebb1a | ||
|
|
c15bdabf0c | ||
|
|
80a077c50a | ||
|
|
42d72c5303 | ||
|
|
74e3990604 | ||
|
|
e6e391ad6a | ||
|
|
12e5099c92 | ||
|
|
72afffbc8d | ||
|
|
627df20b6d | ||
|
|
640bc99661 | ||
|
|
bb43ef15fe | ||
|
|
7907652bc9 | ||
|
|
dce665b792 | ||
|
|
32bf684b17 | ||
|
|
e978bc8690 | ||
|
|
91503d3698 | ||
|
|
1721f6e9e2 | ||
|
|
c7b5aa9197 | ||
|
|
800c8e0a17 | ||
|
|
d771da9abf | ||
|
|
b36b92b247 | ||
|
|
aa51719a55 | ||
|
|
a28685536a | ||
|
|
400fd77d0a | ||
|
|
a2bf974058 | ||
|
|
7c442c9357 | ||
|
|
e17e0a81dd | ||
|
|
2488299d7b | ||
|
|
dfc09b8e04 | ||
|
|
6438baaddc | ||
|
|
8fc6c2ba52 | ||
|
|
95092edc93 | ||
|
|
c4b3e0672e | ||
|
|
e8e2b8ba39 | ||
|
|
2e40b91ec1 | ||
|
|
97d9228bed | ||
|
|
0f14865d56 | ||
|
|
c94ab2a39f | ||
|
|
ab89e055de | ||
|
|
ac6e3dba5a | ||
|
|
d9d2c4a219 | ||
|
|
1890a9378d | ||
|
|
ac88e6e190 | ||
|
|
15cbae8a19 | ||
|
|
aaf096d96d | ||
|
|
93f5acbffd | ||
|
|
9c717275d2 | ||
|
|
50a7c2fad6 | ||
|
|
95be6e721b | ||
|
|
dc2e012cc9 | ||
|
|
12c6282d14 | ||
|
|
0249772f01 | ||
|
|
5d699d5f47 | ||
|
|
34d79a5443 | ||
|
|
1e761e237a | ||
|
|
36ad132c82 | ||
|
|
c9055f391b | ||
|
|
99c95363b4 | ||
|
|
c90526bf07 | ||
|
|
97a6e72e10 | ||
|
|
f100caba81 | ||
|
|
80aa6ffe43 | ||
|
|
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
|
||||
|
||||
49
.eslintrc
49
.eslintrc
@@ -1,35 +1,18 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"extends": ["airbnb-base", "prettier"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"globals": {
|
||||
"Plyr": false,
|
||||
"jQuery": false
|
||||
},
|
||||
"rules": {
|
||||
"import/no-cycle": 1,
|
||||
"no-const-assign": 1,
|
||||
"no-shadow": 0,
|
||||
"no-this-before-super": 1,
|
||||
"no-undef": 1,
|
||||
"no-unreachable": 1,
|
||||
"no-unused-vars": 1,
|
||||
"constructor-super": 1,
|
||||
"valid-typeof": 1,
|
||||
"indent": [2, 4, { "SwitchCase": 1 }],
|
||||
"quotes": [2, "single", "avoid-escape"],
|
||||
"semi": [2, "always"],
|
||||
"eqeqeq": [2, "always"],
|
||||
"one-var": [2, "never"],
|
||||
"comma-dangle": [2, "always-multiline"],
|
||||
"spaced-comment": [2, "always"],
|
||||
"no-restricted-globals": 2,
|
||||
"no-param-reassign": [2, { "props": false }]
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
5
.github/FUNDING.yml
vendored
Normal file
5
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: sampotts
|
||||
patreon: plyr
|
||||
open_collective: plyr
|
||||
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)
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,10 +2,8 @@ node_modules
|
||||
.DS_Store
|
||||
credentials.json
|
||||
*.mp4
|
||||
!dist/blank.mp4
|
||||
index-*.html
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
package-lock.json
|
||||
*.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
|
||||
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
@@ -2,11 +2,11 @@
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
||||
"dbaeumer.vscode-eslint",
|
||||
"wix.vscode-import-cost",
|
||||
"esbenp.prettier-vscode",
|
||||
"shinnn.stylelint",
|
||||
"wayou.vscode-todo-highlight"
|
||||
"wayou.vscode-todo-highlight",
|
||||
"wix.vscode-import-cost",
|
||||
"stylelint.vscode-stylelint",
|
||||
"pflannery.vscode-versionlens"
|
||||
]
|
||||
}
|
||||
|
||||
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,36 +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
|
||||
|
||||
- 💪 **Accessible** - full support for VTT captions and screen readers
|
||||
- 🔧 **[Customisable](#html)** - make the player look how you want with the markup you want
|
||||
- 😎 **Good 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
|
||||
- 📼 **HTML Video & Audio** - support for both formats
|
||||
- 📺 **[Embedded Video](#embeds)** - support for YouTube and Vimeo video playback
|
||||
- 💵 **[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 dependencies** - 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
|
||||
|
||||
@@ -41,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>
|
||||
```
|
||||
|
||||
@@ -67,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>
|
||||
```
|
||||
|
||||
@@ -92,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>
|
||||
```
|
||||
|
||||
@@ -109,32 +115,40 @@ Or the `<div>` non progressively enhanced method:
|
||||
|
||||
## JavaScript
|
||||
|
||||
Include the `plyr.js` script before the closing `</body>` tag and then in your JS create a new instance of Plyr as below.
|
||||
You can use Plyr as an ES6 module as follows:
|
||||
|
||||
```javascript
|
||||
import Plyr from 'plyr';
|
||||
|
||||
const player = new Plyr('#player');
|
||||
```
|
||||
|
||||
Alternatively you can include the `plyr.js` script before the closing `</body>` tag and then in your JS create a new instance of Plyr as below.
|
||||
|
||||
```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.4/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.4/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" />
|
||||
@@ -143,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.4/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.4/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.
|
||||
@@ -204,15 +314,15 @@ 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 that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
|
||||
- A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
|
||||
- A [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.
|
||||
|
||||
#### Single player
|
||||
|
||||
Passing a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList):
|
||||
Passing a CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector):
|
||||
|
||||
```javascript
|
||||
const player = new Plyr('#player');
|
||||
@@ -235,10 +345,10 @@ 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 [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), 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 elements, or a [JQuery](https://jquery.com) object:
|
||||
...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:
|
||||
|
||||
```javascript
|
||||
const players = Plyr.setup('.js-player');
|
||||
@@ -252,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',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -268,8 +378,8 @@ 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. |
|
||||
| `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If you're using the default controls are used then you can specify which settings to show in the menu |
|
||||
| `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. |
|
||||
| `iconUrl` | String | `null` | Specify a URL or path to the SVG sprite. See the [SVG section](#svg) for more info. |
|
||||
@@ -291,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`: Options to display in the menu. Most browsers will refuse to play slower than 0.5. |
|
||||
| `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
|
||||
|
||||
@@ -321,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;
|
||||
});
|
||||
```
|
||||
|
||||
@@ -342,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
|
||||
|
||||
@@ -404,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. |
|
||||
@@ -423,36 +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',
|
||||
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',
|
||||
},
|
||||
],
|
||||
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',
|
||||
},
|
||||
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',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
@@ -460,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',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
@@ -479,41 +596,40 @@ 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',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
_Note:_ `src` property for YouTube and Vimeo can either be the video ID or the whole URL.
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | String | Either `video` or `audio`. _Note:_ YouTube and Vimeo are currently not supported as audio sources. |
|
||||
| `title` | String | _Optional._ Title of the new media. Used for the `aria-label` attribute on the play button, and outer container. YouTube and Vimeo are populated automatically. |
|
||||
| `sources` | Array | This is an array of sources. For HTML5 media, the properties of this object are mapped directly to HTML attributes so more can be added to the object if required. |
|
||||
| `poster`¹ | String | The URL for the poster image (HTML5 video only). |
|
||||
| `tracks`¹ | String | An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above, it will render as `<track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default>` and similar for the French version. Booleans are converted to HTML5 value-less attributes. |
|
||||
| Property | Type | Description |
|
||||
| ------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | String | Either `video` or `audio`. _Note:_ YouTube and Vimeo are currently not supported as audio sources. |
|
||||
| `title` | String | _Optional._ Title of the new media. Used for the `aria-label` attribute on the play button, and outer container. YouTube and Vimeo are populated automatically. |
|
||||
| `sources` | Array | This is an array of sources. For HTML5 media, the properties of this object are mapped directly to HTML attributes so more can be added to the object if required. |
|
||||
| `poster`¹ | String | The URL for the poster image (HTML5 video only). |
|
||||
| `tracks`¹ | String | An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above, it will render as `<track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default>` and similar for the French version. Booleans are converted to HTML5 value-less attributes. |
|
||||
| `previewThumbnails`¹ | Object | The same object like in the `previewThumbnails` constructor option. This means you can either change the thumbnails vtt via the `src` key or disable the thumbnails plugin for the next video by passing `{ enabled: false }`. |
|
||||
|
||||
1. HTML5 only
|
||||
|
||||
@@ -524,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;
|
||||
});
|
||||
```
|
||||
|
||||
@@ -584,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.
|
||||
|
||||
@@ -622,16 +738,16 @@ Fullscreen in Plyr is supported by all browsers that [currently support it](http
|
||||
|
||||
Plyr supports the last 2 versions of most _modern_ browsers.
|
||||
|
||||
| Browser | Supported |
|
||||
| ------------- | ------------- |
|
||||
| Safari | ✓ |
|
||||
| Mobile Safari | ✓¹ |
|
||||
| Firefox | ✓ |
|
||||
| Chrome | ✓ |
|
||||
| Opera | ✓ |
|
||||
| Edge | ✓ |
|
||||
| IE11 | ✓³ |
|
||||
| IE10 | ✓²³ |
|
||||
| Browser | Supported |
|
||||
| ------------- | --------------- |
|
||||
| Safari | ✓ |
|
||||
| Mobile Safari | ✓¹ |
|
||||
| Firefox | ✓ |
|
||||
| Chrome | ✓ |
|
||||
| Opera | ✓ |
|
||||
| Edge | ✓ |
|
||||
| IE11 | ✓³ |
|
||||
| IE10 | ✓<sup>2,3</sup> |
|
||||
|
||||
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide.
|
||||
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)).
|
||||
@@ -651,17 +767,17 @@ 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 programatically
|
||||
## Disable support programmatically
|
||||
|
||||
The `enabled` option can be used to disable certain User Agents. For example, if you don't want to use Plyr for smartphones, you could use:
|
||||
|
||||
```javascript
|
||||
{
|
||||
enabled: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
enabled: !/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -671,14 +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 | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) |
|
||||
| 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) |
|
||||
| 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
|
||||
|
||||
@@ -693,43 +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/)
|
||||
- [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/)
|
||||
|
||||
Let me know on [Twitter](https://twitter.com/sam_potts) I can add you to the above list. It'd be awesome to see how you're using Plyr :-)
|
||||
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
|
||||
|
||||
Credit to the PayPal HTML5 Video player from which Plyr's caption functionality was originally ported from:
|
||||
|
||||
- [PayPal's Accessible HTML5 Video Player](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
|
||||
|
||||
@@ -741,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)
|
||||
@@ -10,13 +10,15 @@
|
||||
"src": "./src/js/plyr.polyfilled.js",
|
||||
"dist": "./dist/",
|
||||
"formats": ["es", "umd"],
|
||||
"namespace": "Plyr"
|
||||
"namespace": "Plyr",
|
||||
"polyfill": true
|
||||
},
|
||||
"demo.js": {
|
||||
"src": "./demo/src/js/demo.js",
|
||||
"dist": "./demo/dist/",
|
||||
"formats": ["iife"],
|
||||
"namespace": "Demo"
|
||||
"namespace": "Demo",
|
||||
"polyfill": true
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
|
||||
1093
changelog.md
1093
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
13219
demo/dist/demo.js
vendored
13219
demo/dist/demo.js
vendored
File diff suppressed because it is too large
Load Diff
2
demo/dist/demo.min.js
vendored
2
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.4 KiB |
1
demo/dist/error.css
vendored
1
demo/dist/error.css
vendored
File diff suppressed because one or more lines are too long
482
demo/index.html
482
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" />
|
||||
|
||||
<!-- Opengraph -->
|
||||
<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"
|
||||
/>
|
||||
</head>
|
||||
<!-- 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"
|
||||
/>
|
||||
|
||||
<body>
|
||||
<div class="grid">
|
||||
<header>
|
||||
<h1>Plyr</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
|
||||
<!-- 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
|
||||
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 monitization 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">
|
||||
<span class="button--with-count">
|
||||
<a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr-button">
|
||||
<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
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
></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"
|
||||
/>
|
||||
|
||||
<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"
|
||||
/>
|
||||
|
||||
<!-- 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/76979871" target="_blank">The New Vimeo Player</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-button"
|
||||
>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>
|
||||
|
||||
<!-- Polyfills -->
|
||||
<script
|
||||
src="https://cdn.polyfill.io/v3/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL,Math.trunc&flags=gated"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
|
||||
<!-- Sharing libary (https://shr.one) -->
|
||||
<script src="https://cdn.shr.one/2.0.0-beta.2/shr.js" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Docs script -->
|
||||
<script src="dist/demo.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
<script src="dist/demo.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
14
demo/package.json
Normal file
14
demo/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"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,287 +1,177 @@
|
||||
// ==========================================================================
|
||||
// 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 Raven from 'raven-js';
|
||||
import './tab-focus';
|
||||
import 'custom-event-polyfill';
|
||||
import 'url-polyfill';
|
||||
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import Shr from 'shr-buttons';
|
||||
|
||||
import Plyr from '../../../src/js/plyr';
|
||||
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';
|
||||
const container = document.getElementById('container');
|
||||
// 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}`)),
|
||||
});
|
||||
}
|
||||
|
||||
if (window.Shr) {
|
||||
window.Shr.setup('.js-shr-button', {
|
||||
count: {
|
||||
classname: 'button__count',
|
||||
},
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const selector = '#player';
|
||||
|
||||
// Setup tab focus
|
||||
const tabClassName = 'tab-focus';
|
||||
|
||||
// Remove class on blur
|
||||
document.addEventListener('focusout', event => {
|
||||
if (!event.target.classList || container.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.target.classList.remove(tabClassName);
|
||||
});
|
||||
|
||||
// Add classname to tabbed elements
|
||||
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;
|
||||
}
|
||||
|
||||
focused.classList.add(tabClassName);
|
||||
}, 10);
|
||||
});
|
||||
|
||||
// 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,
|
||||
},
|
||||
keys: {
|
||||
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
|
||||
},
|
||||
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',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Expose for tinkering in the console
|
||||
window.player = player;
|
||||
|
||||
// Setup type toggle
|
||||
const buttons = document.querySelectorAll('[data-source]');
|
||||
const types = {
|
||||
video: 'video',
|
||||
audio: 'audio',
|
||||
youtube: 'youtube',
|
||||
vimeo: 'vimeo',
|
||||
};
|
||||
let currentType = window.location.hash.replace('#', '');
|
||||
const historySupport = window.history && window.history.pushState;
|
||||
|
||||
// Toggle class on an element
|
||||
function toggleClass(element, className, state) {
|
||||
if (element) {
|
||||
element.classList[state ? 'add' : 'remove'](className);
|
||||
}
|
||||
}
|
||||
|
||||
// Set a new source
|
||||
function newSource(type, init) {
|
||||
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
|
||||
if (
|
||||
!(type in types) ||
|
||||
(!init && type === currentType) ||
|
||||
(!currentType.length && type === types.video)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case types.video:
|
||||
player.source = {
|
||||
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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case types.audio:
|
||||
player.source = {
|
||||
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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case types.youtube:
|
||||
player.source = {
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
|
||||
provider: 'youtube',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case types.vimeo:
|
||||
player.source = {
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://vimeo.com/76979871',
|
||||
provider: 'vimeo',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Set the current type for next time
|
||||
currentType = 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 => {
|
||||
cite.setAttribute('hidden', '');
|
||||
});
|
||||
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
|
||||
}
|
||||
|
||||
// Bind to each button
|
||||
Array.from(buttons).forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const type = button.getAttribute('data-source');
|
||||
|
||||
newSource(type);
|
||||
|
||||
if (historySupport) {
|
||||
window.history.pushState({ type }, '', `#${type}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// List for backwards/forwards
|
||||
window.addEventListener('popstate', event => {
|
||||
if (event.state && 'type' in event.state) {
|
||||
newSource(event.state.type);
|
||||
}
|
||||
});
|
||||
|
||||
// On load
|
||||
if (historySupport) {
|
||||
const video = !currentType.length;
|
||||
|
||||
// If there's no current type set, assume video
|
||||
if (video) {
|
||||
currentType = types.video;
|
||||
}
|
||||
|
||||
// Replace current history state
|
||||
if (currentType in types) {
|
||||
window.history.replaceState(
|
||||
{
|
||||
type: currentType,
|
||||
},
|
||||
'',
|
||||
video ? '' : `#${currentType}`,
|
||||
);
|
||||
}
|
||||
|
||||
// If it's not video, load the source
|
||||
if (currentType !== types.video) {
|
||||
newSource(currentType, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
// 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);
|
||||
});
|
||||
})();
|
||||
|
||||
81
demo/src/js/sources.js
Normal file
81
demo/src/js/sources.js
Normal file
@@ -0,0 +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',
|
||||
},
|
||||
],
|
||||
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;
|
||||
31
demo/src/js/tab-focus.js
Normal file
31
demo/src/js/tab-focus.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// Setup tab focus
|
||||
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;
|
||||
}
|
||||
|
||||
event.target.classList.remove(tabClassName);
|
||||
});
|
||||
|
||||
// Add classname to tabbed elements
|
||||
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;
|
||||
}
|
||||
|
||||
focused.classList.add(tabClassName);
|
||||
}, 10);
|
||||
});
|
||||
5
demo/src/js/toggle-class.js
Normal file
5
demo/src/js/toggle-class.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// Toggle class on an element
|
||||
const toggleClass = (element, className = '', toggle = false) =>
|
||||
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,79 +5,80 @@
|
||||
// Shared
|
||||
.button,
|
||||
.button__count {
|
||||
align-items: center;
|
||||
background: $color-button-background;
|
||||
border: 0;
|
||||
border-radius: $border-radius-base;
|
||||
box-shadow: 0 1px 1px rgba(#000, 0.1);
|
||||
color: $color-button-text;
|
||||
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 {
|
||||
font-weight: $font-weight-bold;
|
||||
padding-left: $spacing-base;
|
||||
padding-right: $spacing-base;
|
||||
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 {
|
||||
color: $gray-dark;
|
||||
&: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);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
&: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 {
|
||||
transform: translateY(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;
|
||||
margin-left: ($spacing-base / 2);
|
||||
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-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,17 +3,28 @@
|
||||
// ==========================================================================
|
||||
|
||||
header {
|
||||
padding-bottom: $spacing-base;
|
||||
text-align: center;
|
||||
padding-bottom: $spacing-base;
|
||||
text-align: center;
|
||||
|
||||
.call-to-action {
|
||||
margin-top: ($spacing-base * 1.5);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $screen-md) {
|
||||
margin-right: ($spacing-base * 3);
|
||||
max-width: 360px;
|
||||
padding-bottom: ($spacing-base * 2);
|
||||
text-align: left;
|
||||
.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: floor($spacing-base / 3);
|
||||
margin-right: math.div($spacing-base, 4);
|
||||
}
|
||||
|
||||
@@ -4,46 +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;
|
||||
font-weight: $font-weight-bold;
|
||||
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,49 +2,37 @@
|
||||
// Examples
|
||||
// ==========================================================================
|
||||
|
||||
// For non supported browsers
|
||||
video {
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@use 'sass:math';
|
||||
|
||||
// Example players
|
||||
.plyr {
|
||||
border-radius: $border-radius-base;
|
||||
box-shadow: 0 2px 5px rgba(#000, 0.2);
|
||||
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 {
|
||||
display: none;
|
||||
margin-top: $spacing-base;
|
||||
color: $color-gray-500;
|
||||
|
||||
.icon {
|
||||
margin-right: ceil($spacing-base / 6);
|
||||
}
|
||||
}
|
||||
|
||||
.plyr--video:not(.plyr--youtube):not(.plyr--vimeo) ~ ul .plyr__cite--video,
|
||||
.plyr--audio ~ ul .plyr__cite--audio,
|
||||
.plyr--youtube ~ ul .plyr__cite--youtube,
|
||||
.plyr--vimeo ~ ul .plyr__cite--vimeo {
|
||||
display: block;
|
||||
.icon {
|
||||
margin-right: math.div($spacing-base, 6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,63 +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;
|
||||
color: $gray;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
padding: ($spacing-base * 0.75);
|
||||
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,11 +3,28 @@
|
||||
// ==========================================================================
|
||||
|
||||
// Fade
|
||||
@keyframes fadein {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@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: 16) {
|
||||
font-size: $size * 1px; // Fallback in px
|
||||
font-size: calculate-rem($size);
|
||||
@mixin font-size($size: $font-size-base) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -2,31 +2,41 @@
|
||||
// Colors
|
||||
// ==========================================================================
|
||||
|
||||
// Greyscale
|
||||
$gray-dark: #343f4a;
|
||||
$gray: #55646b;
|
||||
$gray-light: #cbd0d3;
|
||||
$gray-lighter: #dbe3e8;
|
||||
$off-white: #f2f5f7;
|
||||
// Grayscale
|
||||
$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(198deg 100% 50%);
|
||||
|
||||
// Text
|
||||
$color-text: #fff;
|
||||
|
||||
// Plyr
|
||||
$color-brand-primary: #1aafff;
|
||||
$color-text: $color-gray-700;
|
||||
$color-headings: $color-brand-primary;
|
||||
|
||||
// Brands
|
||||
$color-twitter: #4baaf4;
|
||||
$color-youtube: #cc181e;
|
||||
$color-vimeo: #19b7ed;
|
||||
|
||||
// Elements
|
||||
$color-link: #fff;
|
||||
$color-background: $color-brand-primary;
|
||||
$color-link: $color-brand-primary;
|
||||
|
||||
// Background
|
||||
$color-background-from: hsl(198deg 100% 94%);
|
||||
$color-background-to: hsl(198deg 100% 98%);
|
||||
|
||||
// Buttons
|
||||
$color-button-background: #fff;
|
||||
$color-button-text: $gray;
|
||||
$color-button-background: $color-brand-primary;
|
||||
$color-button-text: #fff;
|
||||
$color-button-background-hover: hsl(198deg 100% 55%);
|
||||
$color-button-count-background: #fff;
|
||||
$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, lighten($color-background, 10%), darken($color-background, 20%));
|
||||
$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,21 +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;
|
||||
|
||||
// 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,
|
||||
)
|
||||
);
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
// Colors
|
||||
// ==========================================================================
|
||||
|
||||
$spacing-base: 20px;
|
||||
$spacing-base: 16px;
|
||||
|
||||
@@ -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,32 +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;
|
||||
text-shadow: 0 1px 1px rgba(#000, 0.15);
|
||||
@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;
|
||||
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,9 +3,11 @@
|
||||
// ==========================================================================
|
||||
|
||||
h1 {
|
||||
@include font-size($font-size-h1);
|
||||
font-weight: $font-weight-bold;
|
||||
letter-spacing: $letter-spacing-headings;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 $spacing-base;
|
||||
@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;
|
||||
}
|
||||
|
||||
80
demo/yarn.lock
Normal file
80
demo/yarn.lock
Normal file
@@ -0,0 +1,80 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@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==
|
||||
|
||||
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==
|
||||
|
||||
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
9137
dist/plyr.js
vendored
9137
dist/plyr.js
vendored
File diff suppressed because it is too large
Load Diff
2
dist/plyr.min.js
vendored
2
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
1
dist/plyr.min.mjs
vendored
1
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
9129
dist/plyr.mjs
vendored
9129
dist/plyr.mjs
vendored
File diff suppressed because it is too large
Load Diff
14954
dist/plyr.polyfilled.js
vendored
14954
dist/plyr.polyfilled.js
vendored
File diff suppressed because it is too large
Load Diff
2
dist/plyr.polyfilled.min.js
vendored
2
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
1
dist/plyr.polyfilled.min.mjs
vendored
1
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
14946
dist/plyr.polyfilled.mjs
vendored
14946
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.4 KiB |
478
gulpfile.js
478
gulpfile.js
@@ -1,482 +1,8 @@
|
||||
// ==========================================================================
|
||||
// Gulp build script
|
||||
// ==========================================================================
|
||||
/* global require, __dirname */
|
||||
/* eslint no-console: "off" */
|
||||
|
||||
const path = require('path');
|
||||
const gulp = require('gulp');
|
||||
const HubRegistry = require('gulp-hub');
|
||||
|
||||
// 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');
|
||||
|
||||
// Deployment
|
||||
const aws = require('aws-sdk');
|
||||
const publish = require('gulp-awspublish');
|
||||
const FastlyPurge = require('fastly-purge');
|
||||
|
||||
const pkg = require('./package.json');
|
||||
const build = require('./build.json');
|
||||
const deploy = require('./deploy.json');
|
||||
|
||||
const { browserslist: browsers, version } = pkg;
|
||||
|
||||
const minSuffix = '.min';
|
||||
|
||||
// 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]) => {
|
||||
entry.formats.forEach(format => {
|
||||
const name = `js:${filename}:${format}`;
|
||||
tasks.js.push(name);
|
||||
const polyfill = filename.includes('polyfilled');
|
||||
const extension = format === 'es' ? 'mjs' : 'js';
|
||||
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(entry.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: entry.namespace,
|
||||
format,
|
||||
},
|
||||
),
|
||||
)
|
||||
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
|
||||
.pipe(
|
||||
rename({
|
||||
extname: `.${extension}`,
|
||||
}),
|
||||
)
|
||||
.pipe(gulp.dest(entry.dist))
|
||||
.pipe(filter(`**/*.${extension}`))
|
||||
.pipe(terser())
|
||||
.pipe(rename({ suffix: minSuffix }))
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(sourcemaps.write(''))
|
||||
.pipe(gulp.dest(entry.dist)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// CSS
|
||||
Object.entries(build.css).forEach(([filename, entry]) => {
|
||||
const name = `css:${filename}`;
|
||||
tasks.css.push(name);
|
||||
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(entry.src)
|
||||
.pipe(plumber())
|
||||
.pipe(sass())
|
||||
.pipe(
|
||||
prefix(browsers, {
|
||||
cascade: false,
|
||||
}),
|
||||
)
|
||||
.pipe(clean())
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(gulp.dest(entry.dist)),
|
||||
);
|
||||
});
|
||||
|
||||
// SVG Sprites
|
||||
Object.entries(build.sprite).forEach(([filename, entry]) => {
|
||||
const name = `sprite:${filename}`;
|
||||
tasks.sprite.push(name);
|
||||
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(entry.src)
|
||||
.pipe(plumber())
|
||||
.pipe(imagemin())
|
||||
.pipe(svgstore())
|
||||
.pipe(rename({ basename: path.parse(filename).name }))
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(gulp.dest(entry.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));
|
||||
});
|
||||
|
||||
// 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', '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(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']));
|
||||
|
||||
191
package.json
191
package.json
@@ -1,95 +1,100 @@
|
||||
{
|
||||
"name": "plyr",
|
||||
"version": "3.5.4",
|
||||
"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",
|
||||
"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",
|
||||
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
|
||||
"deploy": "yarn lint && gulp deploy"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ansi-colors": "^3.2.4",
|
||||
"aws-sdk": "^2.437.0",
|
||||
"@babel/core": "^7.4.3",
|
||||
"@babel/preset-env": "^7.4.3",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"del": "^4.1.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-airbnb-base": "^13.1.0",
|
||||
"eslint-config-prettier": "^4.1.0",
|
||||
"eslint-plugin-import": "^2.16.0",
|
||||
"fancy-log": "^1.3.3",
|
||||
"fastly-purge": "^1.0.1",
|
||||
"git-branch": "^2.0.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-autoprefixer": "^6.0.0",
|
||||
"gulp-awspublish": "^4.0.0",
|
||||
"gulp-better-rollup": "^4.0.1",
|
||||
"gulp-clean-css": "^4.0.0",
|
||||
"gulp-filter": "^5.1.0",
|
||||
"gulp-header": "^2.0.7",
|
||||
"gulp-imagemin": "^5.0.3",
|
||||
"gulp-open": "^3.0.1",
|
||||
"gulp-plumber": "^1.2.1",
|
||||
"gulp-postcss": "^8.0.0",
|
||||
"gulp-rename": "^1.4.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.1.7",
|
||||
"postcss-custom-properties": "^8.0.10",
|
||||
"prettier-eslint": "^8.8.2",
|
||||
"prettier-stylelint": "^0.4.2",
|
||||
"remark-cli": "^6.0.1",
|
||||
"remark-validate-links": "^8.0.2",
|
||||
"rollup": "^1.10.0",
|
||||
"rollup-plugin-babel": "^4.3.2",
|
||||
"rollup-plugin-commonjs": "^9.3.4",
|
||||
"rollup-plugin-node-resolve": "^4.2.3",
|
||||
"stylelint": "^9.10.1",
|
||||
"stylelint-config-prettier": "^5.0.0",
|
||||
"stylelint-config-recommended": "^2.1.0",
|
||||
"stylelint-config-sass-guidelines": "^5.4.0",
|
||||
"stylelint-order": "^2.2.1",
|
||||
"stylelint-scss": "^3.5.4",
|
||||
"stylelint-selector-bem-pattern": "^2.1.0",
|
||||
"through2": "^3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.0.1",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"loadjs": "^3.6.1",
|
||||
"rangetouch": "^2.0.0",
|
||||
"raven-js": "^3.27.0",
|
||||
"url-polyfill": "^1.1.5"
|
||||
}
|
||||
"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,33 +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,
|
||||
|
||||
// Prettier
|
||||
"prettier.eslintIntegration": true,
|
||||
"prettier.stylelintIntegration": true,
|
||||
|
||||
// Formatting
|
||||
"editor.tabSize": 4,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
|
||||
// Trim on save
|
||||
"files.trimTrailingWhitespace": 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,368 +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]));
|
||||
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();
|
||||
|
||||
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
|
||||
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(track => track.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,426 +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.4/plyr.svg',
|
||||
|
||||
// Blank video (used to prevent errors on source change)
|
||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||
|
||||
// Quality default
|
||||
quality: {
|
||||
default: 576,
|
||||
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
|
||||
// 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,
|
||||
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
|
||||
},
|
||||
|
||||
// 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',
|
||||
'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',
|
||||
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}', // 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title),fileDetails)&part=snippet',
|
||||
},
|
||||
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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
3275
src/js/controls.js
vendored
3275
src/js/controls.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -4,41 +4,166 @@
|
||||
// https://webkit.org/blog/7929/designing-websites-for-iphone-x/
|
||||
// ==========================================================================
|
||||
|
||||
import { repaint } from './utils/animation';
|
||||
import browser from './utils/browser';
|
||||
import { hasClass, toggleClass, trapFocus } 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';
|
||||
|
||||
function onChange() {
|
||||
if (!this.enabled) {
|
||||
class Fullscreen {
|
||||
constructor(player) {
|
||||
// Keep reference to parent
|
||||
this.player = player;
|
||||
|
||||
// Get prefix
|
||||
this.prefix = Fullscreen.prefix;
|
||||
this.property = Fullscreen.property;
|
||||
|
||||
// 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';
|
||||
|
||||
// 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);
|
||||
|
||||
// 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 '';
|
||||
}
|
||||
|
||||
// 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
|
||||
? this.target.getRootNode().fullscreenElement
|
||||
: this.target.getRootNode()[`${this.prefix}${this.property}Element`];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Update toggle button
|
||||
const button = this.player.elements.buttons.fullscreen;
|
||||
if (is.element(button)) {
|
||||
button.pressed = this.active;
|
||||
button.pressed = this.active;
|
||||
}
|
||||
|
||||
// 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, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
|
||||
triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
|
||||
};
|
||||
|
||||
// Trap focus in container
|
||||
if (!browser.isIos) {
|
||||
trapFocus.call(this.player, this.target, this.active);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFallback(toggle = false) {
|
||||
toggleFallback = (toggle = false) => {
|
||||
// Store or restore scroll position
|
||||
if (toggle) {
|
||||
this.scrollPosition = {
|
||||
x: window.scrollX || 0,
|
||||
y: window.scrollY || 0,
|
||||
};
|
||||
this.scrollPosition = {
|
||||
x: window.scrollX || 0,
|
||||
y: window.scrollY || 0,
|
||||
};
|
||||
} else {
|
||||
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
|
||||
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
|
||||
}
|
||||
|
||||
// Toggle scroll
|
||||
@@ -49,223 +174,132 @@ function toggleFallback(toggle = false) {
|
||||
|
||||
// Force full viewport on iPhone X+
|
||||
if (browser.isIos) {
|
||||
let viewport = document.head.querySelector('meta[name="viewport"]');
|
||||
const property = 'viewport-fit=cover';
|
||||
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');
|
||||
// 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}`;
|
||||
}
|
||||
|
||||
// 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(',');
|
||||
}
|
||||
|
||||
// Force a repaint as sometimes Safari doesn't want to fill the screen
|
||||
setTimeout(() => repaint(this.target), 100);
|
||||
} else if (this.cleanupViewport) {
|
||||
viewport.content = viewport.content
|
||||
.split(',')
|
||||
.filter((part) => part.trim() !== property)
|
||||
.join(',');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle button and fire events
|
||||
onChange.call(this);
|
||||
}
|
||||
this.onChange();
|
||||
};
|
||||
|
||||
class Fullscreen {
|
||||
constructor(player) {
|
||||
// Keep reference to parent
|
||||
this.player = player;
|
||||
|
||||
// Get prefix
|
||||
this.prefix = Fullscreen.prefix;
|
||||
this.property = Fullscreen.property;
|
||||
|
||||
// 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';
|
||||
|
||||
// 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??
|
||||
onChange.call(this);
|
||||
},
|
||||
);
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
// Update the UI
|
||||
this.update();
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Determine if native supported
|
||||
static get native() {
|
||||
return !!(
|
||||
document.fullscreenEnabled ||
|
||||
document.webkitFullscreenEnabled ||
|
||||
document.mozFullScreenEnabled ||
|
||||
document.msFullscreenEnabled
|
||||
);
|
||||
// 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');
|
||||
}
|
||||
|
||||
// If we're actually using native
|
||||
get usingNative() {
|
||||
return Fullscreen.native && !this.forceFallback;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Get the prefix for handlers
|
||||
static get prefix() {
|
||||
// No prefix
|
||||
if (is.function(document.exitFullscreen)) {
|
||||
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}`]();
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
// Bail from fullscreen
|
||||
exit = () => {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
static get property() {
|
||||
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
|
||||
// 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}`]();
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
this.target.webkitEnterFullscreen();
|
||||
} else if (!Fullscreen.native || this.forceFallback) {
|
||||
toggleFallback.call(this, true);
|
||||
} else if (!this.prefix) {
|
||||
this.target.requestFullscreen();
|
||||
} 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();
|
||||
this.player.play();
|
||||
} else if (!Fullscreen.native || this.forceFallback) {
|
||||
toggleFallback.call(this, 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();
|
||||
}
|
||||
// Toggle state
|
||||
toggle = () => {
|
||||
if (!this.active) {
|
||||
this.enter();
|
||||
} else {
|
||||
this.exit();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Fullscreen;
|
||||
|
||||
226
src/js/html5.js
226
src/js/html5.js
@@ -6,122 +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() {
|
||||
// Get sizes from <source> elements
|
||||
return html5.getSources
|
||||
.call(this)
|
||||
.map(source => Number(source.getAttribute('size')))
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
extend() {
|
||||
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 aspect ratio if set
|
||||
setAspectRatio.call(player);
|
||||
|
||||
// Quality
|
||||
Object.defineProperty(player.media, 'quality', {
|
||||
get() {
|
||||
// Get sources
|
||||
const sources = html5.getSources.call(player);
|
||||
const source = sources.find(source => source.getAttribute('src') === player.source);
|
||||
|
||||
// Return size, if match is found
|
||||
return source && Number(source.getAttribute('size'));
|
||||
},
|
||||
set(input) {
|
||||
// Get sources
|
||||
const sources = html5.getSources.call(player);
|
||||
|
||||
// Get first match for requested size
|
||||
const source = sources.find(source => Number(source.getAttribute('size')) === input);
|
||||
|
||||
// No matching source found
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current state
|
||||
const { currentTime, paused, preload, readyState } = 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.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;
|
||||
|
||||
1655
src/js/listeners.js
1655
src/js/listeners.js
File diff suppressed because it is too large
Load Diff
@@ -8,52 +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
|
||||
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.extend.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
718
src/js/plugins/preview-thumbnails.js
Normal file
718
src/js/plugins/preview-thumbnails.js
Normal file
@@ -0,0 +1,718 @@
|
||||
import { createElement } from '../utils/elements';
|
||||
import { once } from '../utils/events';
|
||||
import fetch from '../utils/fetch';
|
||||
import is from '../utils/is';
|
||||
import { formatTime } from '../utils/time';
|
||||
|
||||
// Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg"
|
||||
const parseVtt = (vttDataString) => {
|
||||
const processedList = [];
|
||||
const frames = vttDataString.split(/\r\n\r\n|\n\n|\r\r/);
|
||||
|
||||
frames.forEach((frame) => {
|
||||
const result = {};
|
||||
const lines = frame.split(/\r\n|\n|\r/);
|
||||
|
||||
lines.forEach((line) => {
|
||||
if (!is.number(result.startTime)) {
|
||||
// The line with start and end times on it is the first line of interest
|
||||
const matchTimes = line.match(
|
||||
/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,
|
||||
); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT
|
||||
|
||||
if (matchTimes) {
|
||||
result.startTime =
|
||||
Number(matchTimes[1] || 0) * 60 * 60 +
|
||||
Number(matchTimes[2]) * 60 +
|
||||
Number(matchTimes[3]) +
|
||||
Number(`0.${matchTimes[4]}`);
|
||||
result.endTime =
|
||||
Number(matchTimes[6] || 0) * 60 * 60 +
|
||||
Number(matchTimes[7]) * 60 +
|
||||
Number(matchTimes[8]) +
|
||||
Number(`0.${matchTimes[9]}`);
|
||||
}
|
||||
} else if (!is.empty(line.trim()) && is.empty(result.text)) {
|
||||
// If we already have the startTime, then we're definitely up to the text line(s)
|
||||
const lineSplit = line.trim().split('#xywh=');
|
||||
[result.text] = lineSplit;
|
||||
|
||||
// If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image
|
||||
if (lineSplit[1]) {
|
||||
[result.x, result.y, result.w, result.h] = lineSplit[1].split(',');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (result.text) {
|
||||
processedList.push(result);
|
||||
}
|
||||
});
|
||||
|
||||
return processedList;
|
||||
};
|
||||
|
||||
/**
|
||||
* Preview thumbnails for seek hover and scrubbing
|
||||
* Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar
|
||||
* Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed
|
||||
*
|
||||
* Notes:
|
||||
* - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole
|
||||
* - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails
|
||||
* - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered
|
||||
*/
|
||||
|
||||
const fitRatio = (ratio, outer) => {
|
||||
const targetRatio = outer.width / outer.height;
|
||||
const result = {};
|
||||
if (ratio > targetRatio) {
|
||||
result.width = outer.width;
|
||||
result.height = (1 / ratio) * outer.width;
|
||||
} else {
|
||||
result.height = outer.height;
|
||||
result.width = ratio * outer.height;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
class PreviewThumbnails {
|
||||
/**
|
||||
* PreviewThumbnails constructor.
|
||||
* @param {Plyr} player
|
||||
* @return {PreviewThumbnails}
|
||||
*/
|
||||
constructor(player) {
|
||||
this.player = player;
|
||||
this.thumbnails = [];
|
||||
this.loaded = false;
|
||||
this.lastMouseMoveTime = Date.now();
|
||||
this.mouseDown = false;
|
||||
this.loadedImages = [];
|
||||
|
||||
this.elements = {
|
||||
thumb: {},
|
||||
scrubbing: {},
|
||||
};
|
||||
|
||||
this.load();
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;
|
||||
}
|
||||
|
||||
load = () => {
|
||||
// Toggle the regular seek tooltip
|
||||
if (this.player.elements.display.seekTooltip) {
|
||||
this.player.elements.display.seekTooltip.hidden = this.enabled;
|
||||
}
|
||||
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getThumbnails().then(() => {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Render DOM elements
|
||||
this.render();
|
||||
|
||||
// Check to see if thumb container size was specified manually in CSS
|
||||
this.determineContainerAutoSizing();
|
||||
|
||||
this.loaded = true;
|
||||
});
|
||||
};
|
||||
|
||||
// Download VTT files and parse them
|
||||
getThumbnails = () => {
|
||||
return new Promise((resolve) => {
|
||||
const { src } = this.player.config.previewThumbnails;
|
||||
|
||||
if (is.empty(src)) {
|
||||
throw new Error('Missing previewThumbnails.src config attribute');
|
||||
}
|
||||
|
||||
// Resolve promise
|
||||
const sortAndResolve = () => {
|
||||
// Sort smallest to biggest (e.g., [120p, 480p, 1080p])
|
||||
this.thumbnails.sort((x, y) => x.height - y.height);
|
||||
|
||||
this.player.debug.log('Preview thumbnails', this.thumbnails);
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
// Via callback()
|
||||
if (is.function(src)) {
|
||||
src((thumbnails) => {
|
||||
this.thumbnails = thumbnails;
|
||||
sortAndResolve();
|
||||
});
|
||||
}
|
||||
// VTT urls
|
||||
else {
|
||||
// If string, convert into single-element list
|
||||
const urls = is.string(src) ? [src] : src;
|
||||
// Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
|
||||
const promises = urls.map((u) => this.getThumbnail(u));
|
||||
// Resolve
|
||||
Promise.all(promises).then(sortAndResolve);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Process individual VTT file
|
||||
getThumbnail = (url) => {
|
||||
return new Promise((resolve) => {
|
||||
fetch(url).then((response) => {
|
||||
const thumbnail = {
|
||||
frames: parseVtt(response),
|
||||
height: null,
|
||||
urlPrefix: '',
|
||||
};
|
||||
|
||||
// If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file
|
||||
// If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank
|
||||
// If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file
|
||||
if (
|
||||
!thumbnail.frames[0].text.startsWith('/') &&
|
||||
!thumbnail.frames[0].text.startsWith('http://') &&
|
||||
!thumbnail.frames[0].text.startsWith('https://')
|
||||
) {
|
||||
thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
// Download the first frame, so that we can determine/set the height of this thumbnailsDef
|
||||
const tempImage = new Image();
|
||||
|
||||
tempImage.onload = () => {
|
||||
thumbnail.height = tempImage.naturalHeight;
|
||||
thumbnail.width = tempImage.naturalWidth;
|
||||
|
||||
this.thumbnails.push(thumbnail);
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
startMove = (event) => {
|
||||
if (!this.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until media has a duration
|
||||
if (!this.player.media.duration) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'touchmove') {
|
||||
// Calculate seek hover position as approx video seconds
|
||||
this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);
|
||||
} else {
|
||||
// Calculate seek hover position as approx video seconds
|
||||
const clientRect = this.player.elements.progress.getBoundingClientRect();
|
||||
const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left);
|
||||
this.seekTime = this.player.media.duration * (percentage / 100);
|
||||
|
||||
if (this.seekTime < 0) {
|
||||
// The mousemove fires for 10+px out to the left
|
||||
this.seekTime = 0;
|
||||
}
|
||||
|
||||
if (this.seekTime > this.player.media.duration - 1) {
|
||||
// Took 1 second off the duration for safety, because different players can disagree on the real duration of a video
|
||||
this.seekTime = this.player.media.duration - 1;
|
||||
}
|
||||
|
||||
this.mousePosX = event.pageX;
|
||||
|
||||
// Set time text inside image container
|
||||
this.elements.thumb.time.innerText = formatTime(this.seekTime);
|
||||
|
||||
// Get marker point for time
|
||||
const point = this.player.config.markers?.points?.find(({ time: t }) => t === Math.round(this.seekTime));
|
||||
|
||||
// Append the point label to the tooltip
|
||||
if (point) {
|
||||
// this.elements.thumb.time.innerText.concat('\n');
|
||||
this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);
|
||||
}
|
||||
}
|
||||
|
||||
// Download and show image
|
||||
this.showImageAtCurrentTime();
|
||||
};
|
||||
|
||||
endMove = () => {
|
||||
this.toggleThumbContainer(false, true);
|
||||
};
|
||||
|
||||
startScrubbing = (event) => {
|
||||
// Only act on left mouse button (0), or touch device (event.button does not exist or is false)
|
||||
if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {
|
||||
this.mouseDown = true;
|
||||
|
||||
// Wait until media has a duration
|
||||
if (this.player.media.duration) {
|
||||
this.toggleScrubbingContainer(true);
|
||||
this.toggleThumbContainer(false, true);
|
||||
|
||||
// Download and show image
|
||||
this.showImageAtCurrentTime();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
endScrubbing = () => {
|
||||
this.mouseDown = false;
|
||||
|
||||
// Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview
|
||||
if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {
|
||||
// The video was already seeked/loaded at the chosen time - hide immediately
|
||||
this.toggleScrubbingContainer(false);
|
||||
} else {
|
||||
// The video hasn't seeked yet. Wait for that
|
||||
once.call(this.player, this.player.media, 'timeupdate', () => {
|
||||
// Re-check mousedown - we might have already started scrubbing again
|
||||
if (!this.mouseDown) {
|
||||
this.toggleScrubbingContainer(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup hooks for Plyr and window events
|
||||
*/
|
||||
listeners = () => {
|
||||
// Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering
|
||||
this.player.on('play', () => {
|
||||
this.toggleThumbContainer(false, true);
|
||||
});
|
||||
|
||||
this.player.on('seeked', () => {
|
||||
this.toggleThumbContainer(false);
|
||||
});
|
||||
|
||||
this.player.on('timeupdate', () => {
|
||||
this.lastTime = this.player.media.currentTime;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create HTML elements for image containers
|
||||
*/
|
||||
render = () => {
|
||||
// Create HTML element: plyr__preview-thumbnail-container
|
||||
this.elements.thumb.container = createElement('div', {
|
||||
class: this.player.config.classNames.previewThumbnails.thumbContainer,
|
||||
});
|
||||
|
||||
// Wrapper for the image for styling
|
||||
this.elements.thumb.imageContainer = createElement('div', {
|
||||
class: this.player.config.classNames.previewThumbnails.imageContainer,
|
||||
});
|
||||
this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);
|
||||
|
||||
// Create HTML element, parent+span: time text (e.g., 01:32:00)
|
||||
const timeContainer = createElement('div', {
|
||||
class: this.player.config.classNames.previewThumbnails.timeContainer,
|
||||
});
|
||||
|
||||
this.elements.thumb.time = createElement('span', {}, '00:00');
|
||||
timeContainer.appendChild(this.elements.thumb.time);
|
||||
|
||||
this.elements.thumb.imageContainer.appendChild(timeContainer);
|
||||
|
||||
// Inject the whole thumb
|
||||
if (is.element(this.player.elements.progress)) {
|
||||
this.player.elements.progress.appendChild(this.elements.thumb.container);
|
||||
}
|
||||
|
||||
// Create HTML element: plyr__preview-scrubbing-container
|
||||
this.elements.scrubbing.container = createElement('div', {
|
||||
class: this.player.config.classNames.previewThumbnails.scrubbingContainer,
|
||||
});
|
||||
|
||||
this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);
|
||||
};
|
||||
|
||||
destroy = () => {
|
||||
if (this.elements.thumb.container) {
|
||||
this.elements.thumb.container.remove();
|
||||
}
|
||||
if (this.elements.scrubbing.container) {
|
||||
this.elements.scrubbing.container.remove();
|
||||
}
|
||||
};
|
||||
|
||||
showImageAtCurrentTime = () => {
|
||||
if (this.mouseDown) {
|
||||
this.setScrubbingContainerSize();
|
||||
} else {
|
||||
this.setThumbContainerSizeAndPos();
|
||||
}
|
||||
|
||||
// Find the desired thumbnail index
|
||||
// TODO: Handle a video longer than the thumbs where thumbNum is null
|
||||
const thumbNum = this.thumbnails[0].frames.findIndex(
|
||||
(frame) => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,
|
||||
);
|
||||
const hasThumb = thumbNum >= 0;
|
||||
let qualityIndex = 0;
|
||||
|
||||
// Show the thumb container if we're not scrubbing
|
||||
if (!this.mouseDown) {
|
||||
this.toggleThumbContainer(hasThumb);
|
||||
}
|
||||
|
||||
// No matching thumb found
|
||||
if (!hasThumb) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if we've already downloaded higher quality versions of this image
|
||||
this.thumbnails.forEach((thumbnail, index) => {
|
||||
if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {
|
||||
qualityIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
// Only proceed if either thumbnum or thumbfilename has changed
|
||||
if (thumbNum !== this.showingThumb) {
|
||||
this.showingThumb = thumbNum;
|
||||
this.loadImage(qualityIndex);
|
||||
}
|
||||
};
|
||||
|
||||
// Show the image that's currently specified in this.showingThumb
|
||||
loadImage = (qualityIndex = 0) => {
|
||||
const thumbNum = this.showingThumb;
|
||||
const thumbnail = this.thumbnails[qualityIndex];
|
||||
const { urlPrefix } = thumbnail;
|
||||
const frame = thumbnail.frames[thumbNum];
|
||||
const thumbFilename = thumbnail.frames[thumbNum].text;
|
||||
const thumbUrl = urlPrefix + thumbFilename;
|
||||
|
||||
if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {
|
||||
// If we're already loading a previous image, remove its onload handler - we don't want it to load after this one
|
||||
// Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort
|
||||
if (this.loadingImage && this.usingSprites) {
|
||||
this.loadingImage.onload = null;
|
||||
}
|
||||
|
||||
// We're building and adding a new image. In other implementations of similar functionality (YouTube), background image
|
||||
// is instead used. But this causes issues with larger images in Firefox and Safari - switching between background
|
||||
// images causes a flicker. Putting a new image over the top does not
|
||||
const previewImage = new Image();
|
||||
previewImage.src = thumbUrl;
|
||||
previewImage.dataset.index = thumbNum;
|
||||
previewImage.dataset.filename = thumbFilename;
|
||||
this.showingThumbFilename = thumbFilename;
|
||||
|
||||
this.player.debug.log(`Loading image: ${thumbUrl}`);
|
||||
|
||||
// For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...
|
||||
previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);
|
||||
this.loadingImage = previewImage;
|
||||
this.removeOldImages(previewImage);
|
||||
} else {
|
||||
// Update the existing image
|
||||
this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);
|
||||
this.currentImageElement.dataset.index = thumbNum;
|
||||
this.removeOldImages(this.currentImageElement);
|
||||
}
|
||||
};
|
||||
|
||||
showImage = (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {
|
||||
this.player.debug.log(
|
||||
`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`,
|
||||
);
|
||||
this.setImageSizeAndOffset(previewImage, frame);
|
||||
|
||||
if (newImage) {
|
||||
this.currentImageContainer.appendChild(previewImage);
|
||||
this.currentImageElement = previewImage;
|
||||
|
||||
if (!this.loadedImages.includes(thumbFilename)) {
|
||||
this.loadedImages.push(thumbFilename);
|
||||
}
|
||||
}
|
||||
|
||||
// Preload images before and after the current one
|
||||
// Show higher quality of the same frame
|
||||
// Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading
|
||||
this.preloadNearby(thumbNum, true)
|
||||
.then(this.preloadNearby(thumbNum, false))
|
||||
.then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));
|
||||
};
|
||||
|
||||
// Remove all preview images that aren't the designated current image
|
||||
removeOldImages = (currentImage) => {
|
||||
// Get a list of all images, convert it from a DOM list to an array
|
||||
Array.from(this.currentImageContainer.children).forEach((image) => {
|
||||
if (image.tagName.toLowerCase() !== 'img') {
|
||||
return;
|
||||
}
|
||||
|
||||
const removeDelay = this.usingSprites ? 500 : 1000;
|
||||
|
||||
if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {
|
||||
// Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients
|
||||
// First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
image.dataset.deleting = true;
|
||||
|
||||
// This has to be set before the timeout - to prevent issues switching between hover and scrub
|
||||
const { currentImageContainer } = this;
|
||||
|
||||
setTimeout(() => {
|
||||
currentImageContainer.removeChild(image);
|
||||
this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);
|
||||
}, removeDelay);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Preload images before and after the current one. Only if the user is still hovering/seeking the same frame
|
||||
// This will only preload the lowest quality
|
||||
preloadNearby = (thumbNum, forward = true) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;
|
||||
|
||||
if (this.showingThumbFilename === oldThumbFilename) {
|
||||
// Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away
|
||||
let thumbnailsClone;
|
||||
if (forward) {
|
||||
thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);
|
||||
} else {
|
||||
thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();
|
||||
}
|
||||
|
||||
let foundOne = false;
|
||||
|
||||
thumbnailsClone.forEach((frame) => {
|
||||
const newThumbFilename = frame.text;
|
||||
|
||||
if (newThumbFilename !== oldThumbFilename) {
|
||||
// Found one with a different filename. Make sure it hasn't already been loaded on this page visit
|
||||
if (!this.loadedImages.includes(newThumbFilename)) {
|
||||
foundOne = true;
|
||||
this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);
|
||||
|
||||
const { urlPrefix } = this.thumbnails[0];
|
||||
const thumbURL = urlPrefix + newThumbFilename;
|
||||
const previewImage = new Image();
|
||||
previewImage.src = thumbURL;
|
||||
previewImage.onload = () => {
|
||||
this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);
|
||||
if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);
|
||||
|
||||
// We don't resolve until the thumb is loaded
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If there are none to preload then we want to resolve immediately
|
||||
if (!foundOne) {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
};
|
||||
|
||||
// If user has been hovering current image for half a second, look for a higher quality one
|
||||
getHigherQuality = (currentQualityIndex, previewImage, frame, thumbFilename) => {
|
||||
if (currentQualityIndex < this.thumbnails.length - 1) {
|
||||
// Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container
|
||||
let previewImageHeight = previewImage.naturalHeight;
|
||||
|
||||
if (this.usingSprites) {
|
||||
previewImageHeight = frame.h;
|
||||
}
|
||||
|
||||
if (previewImageHeight < this.thumbContainerHeight) {
|
||||
// Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while
|
||||
setTimeout(() => {
|
||||
// Make sure the mouse hasn't already moved on and started hovering at another image
|
||||
if (this.showingThumbFilename === thumbFilename) {
|
||||
this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);
|
||||
this.loadImage(currentQualityIndex + 1);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
get currentImageContainer() {
|
||||
if (this.mouseDown) {
|
||||
return this.elements.scrubbing.container;
|
||||
}
|
||||
|
||||
return this.elements.thumb.imageContainer;
|
||||
}
|
||||
|
||||
get usingSprites() {
|
||||
return Object.keys(this.thumbnails[0].frames[0]).includes('w');
|
||||
}
|
||||
|
||||
get thumbAspectRatio() {
|
||||
if (this.usingSprites) {
|
||||
return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;
|
||||
}
|
||||
|
||||
return this.thumbnails[0].width / this.thumbnails[0].height;
|
||||
}
|
||||
|
||||
get thumbContainerHeight() {
|
||||
if (this.mouseDown) {
|
||||
const { height } = fitRatio(this.thumbAspectRatio, {
|
||||
width: this.player.media.clientWidth,
|
||||
height: this.player.media.clientHeight,
|
||||
});
|
||||
return height;
|
||||
}
|
||||
|
||||
// If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)
|
||||
if (this.sizeSpecifiedInCSS) {
|
||||
return this.elements.thumb.imageContainer.clientHeight;
|
||||
}
|
||||
|
||||
return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);
|
||||
}
|
||||
|
||||
get currentImageElement() {
|
||||
if (this.mouseDown) {
|
||||
return this.currentScrubbingImageElement;
|
||||
}
|
||||
|
||||
return this.currentThumbnailImageElement;
|
||||
}
|
||||
|
||||
set currentImageElement(element) {
|
||||
if (this.mouseDown) {
|
||||
this.currentScrubbingImageElement = element;
|
||||
} else {
|
||||
this.currentThumbnailImageElement = element;
|
||||
}
|
||||
}
|
||||
|
||||
toggleThumbContainer = (toggle = false, clearShowing = false) => {
|
||||
const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;
|
||||
this.elements.thumb.container.classList.toggle(className, toggle);
|
||||
|
||||
if (!toggle && clearShowing) {
|
||||
this.showingThumb = null;
|
||||
this.showingThumbFilename = null;
|
||||
}
|
||||
};
|
||||
|
||||
toggleScrubbingContainer = (toggle = false) => {
|
||||
const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;
|
||||
this.elements.scrubbing.container.classList.toggle(className, toggle);
|
||||
|
||||
if (!toggle) {
|
||||
this.showingThumb = null;
|
||||
this.showingThumbFilename = null;
|
||||
}
|
||||
};
|
||||
|
||||
determineContainerAutoSizing = () => {
|
||||
if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {
|
||||
// This will prevent auto sizing in this.setThumbContainerSizeAndPos()
|
||||
this.sizeSpecifiedInCSS = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS
|
||||
setThumbContainerSizeAndPos = () => {
|
||||
if (!this.sizeSpecifiedInCSS) {
|
||||
const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);
|
||||
this.elements.thumb.imageContainer.style.height = `${this.thumbContainerHeight}px`;
|
||||
this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`;
|
||||
} else if (
|
||||
this.elements.thumb.imageContainer.clientHeight > 20 &&
|
||||
this.elements.thumb.imageContainer.clientWidth < 20
|
||||
) {
|
||||
const thumbWidth = Math.floor(this.elements.thumb.imageContainer.clientHeight * this.thumbAspectRatio);
|
||||
this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`;
|
||||
} else if (
|
||||
this.elements.thumb.imageContainer.clientHeight < 20 &&
|
||||
this.elements.thumb.imageContainer.clientWidth > 20
|
||||
) {
|
||||
const thumbHeight = Math.floor(this.elements.thumb.imageContainer.clientWidth / this.thumbAspectRatio);
|
||||
this.elements.thumb.imageContainer.style.height = `${thumbHeight}px`;
|
||||
}
|
||||
|
||||
this.setThumbContainerPos();
|
||||
};
|
||||
|
||||
setThumbContainerPos = () => {
|
||||
const seekbarRect = this.player.elements.progress.getBoundingClientRect();
|
||||
const plyrRect = this.player.elements.container.getBoundingClientRect();
|
||||
const { container } = this.elements.thumb;
|
||||
// Find the lowest and highest desired left-position, so we don't slide out the side of the video container
|
||||
const minVal = plyrRect.left - seekbarRect.left + 10;
|
||||
const maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10;
|
||||
// Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth
|
||||
let previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2;
|
||||
|
||||
if (previewPos < minVal) {
|
||||
previewPos = minVal;
|
||||
}
|
||||
|
||||
if (previewPos > maxVal) {
|
||||
previewPos = maxVal;
|
||||
}
|
||||
|
||||
container.style.left = `${previewPos}px`;
|
||||
};
|
||||
|
||||
// Can't use 100% width, in case the video is a different aspect ratio to the video container
|
||||
setScrubbingContainerSize = () => {
|
||||
const { width, height } = fitRatio(this.thumbAspectRatio, {
|
||||
width: this.player.media.clientWidth,
|
||||
height: this.player.media.clientHeight,
|
||||
});
|
||||
this.elements.scrubbing.container.style.width = `${width}px`;
|
||||
this.elements.scrubbing.container.style.height = `${height}px`;
|
||||
};
|
||||
|
||||
// Sprites need to be offset to the correct location
|
||||
setImageSizeAndOffset = (previewImage, frame) => {
|
||||
if (!this.usingSprites) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find difference between height and preview container height
|
||||
const multiplier = this.thumbContainerHeight / frame.h;
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
previewImage.style.left = `-${frame.x * multiplier}px`;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
previewImage.style.top = `-${frame.y * multiplier}px`;
|
||||
};
|
||||
}
|
||||
|
||||
export default PreviewThumbnails;
|
||||
@@ -1,646 +0,0 @@
|
||||
import { createElement } from '../utils/elements';
|
||||
import { once } from '../utils/events';
|
||||
import fetch from '../utils/fetch';
|
||||
import is from '../utils/is';
|
||||
import { formatTime } from '../utils/time';
|
||||
|
||||
// Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg"
|
||||
const parseVtt = vttDataString => {
|
||||
const processedList = [];
|
||||
const frames = vttDataString.split(/\r\n\r\n|\n\n|\r\r/);
|
||||
|
||||
frames.forEach(frame => {
|
||||
const result = {};
|
||||
const lines = frame.split(/\r\n|\n|\r/);
|
||||
|
||||
lines.forEach(line => {
|
||||
if (!is.number(result.startTime)) {
|
||||
// The line with start and end times on it is the first line of interest
|
||||
const matchTimes = line.match(
|
||||
/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,
|
||||
); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT
|
||||
|
||||
if (matchTimes) {
|
||||
result.startTime =
|
||||
Number(matchTimes[1] || 0) * 60 * 60 +
|
||||
Number(matchTimes[2]) * 60 +
|
||||
Number(matchTimes[3]) +
|
||||
Number(`0.${matchTimes[4]}`);
|
||||
result.endTime =
|
||||
Number(matchTimes[6] || 0) * 60 * 60 +
|
||||
Number(matchTimes[7]) * 60 +
|
||||
Number(matchTimes[8]) +
|
||||
Number(`0.${matchTimes[9]}`);
|
||||
}
|
||||
} else if (!is.empty(line.trim()) && is.empty(result.text)) {
|
||||
// If we already have the startTime, then we're definitely up to the text line(s)
|
||||
const lineSplit = line.trim().split('#xywh=');
|
||||
[result.text] = lineSplit;
|
||||
|
||||
// If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image
|
||||
if (lineSplit[1]) {
|
||||
[result.x, result.y, result.w, result.h] = lineSplit[1].split(',');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (result.text) {
|
||||
processedList.push(result);
|
||||
}
|
||||
});
|
||||
|
||||
return processedList;
|
||||
};
|
||||
|
||||
/**
|
||||
* Preview thumbnails for seek hover and scrubbing
|
||||
* Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar
|
||||
* Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed
|
||||
*
|
||||
* Notes:
|
||||
* - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole
|
||||
* - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails
|
||||
* - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered
|
||||
*/
|
||||
|
||||
class PreviewThumbnails {
|
||||
/**
|
||||
* PreviewThumbnails constructor.
|
||||
* @param {Plyr} player
|
||||
* @return {PreviewThumbnails}
|
||||
*/
|
||||
constructor(player) {
|
||||
this.player = player;
|
||||
this.thumbnails = [];
|
||||
this.loaded = false;
|
||||
this.lastMouseMoveTime = Date.now();
|
||||
this.mouseDown = false;
|
||||
this.loadedImages = [];
|
||||
|
||||
this.elements = {
|
||||
thumb: {},
|
||||
scrubbing: {},
|
||||
};
|
||||
|
||||
this.load();
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;
|
||||
}
|
||||
|
||||
load() {
|
||||
// Togglethe regular seek tooltip
|
||||
if (this.player.elements.display.seekTooltip) {
|
||||
this.player.elements.display.seekTooltip.hidden = this.enabled;
|
||||
}
|
||||
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getThumbnails().then(() => {
|
||||
// Render DOM elements
|
||||
this.render();
|
||||
|
||||
// Check to see if thumb container size was specified manually in CSS
|
||||
this.determineContainerAutoSizing();
|
||||
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Download VTT files and parse them
|
||||
getThumbnails() {
|
||||
return new Promise(resolve => {
|
||||
const { src } = this.player.config.previewThumbnails;
|
||||
|
||||
if (is.empty(src)) {
|
||||
throw new Error('Missing previewThumbnails.src config attribute');
|
||||
}
|
||||
|
||||
// If string, convert into single-element list
|
||||
const urls = is.string(src) ? [src] : src;
|
||||
|
||||
// Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
|
||||
const promises = urls.map(u => this.getThumbnail(u));
|
||||
|
||||
Promise.all(promises).then(() => {
|
||||
// Sort smallest to biggest (e.g., [120p, 480p, 1080p])
|
||||
this.thumbnails.sort((x, y) => x.height - y.height);
|
||||
|
||||
this.player.debug.log('Preview thumbnails', this.thumbnails);
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Process individual VTT file
|
||||
getThumbnail(url) {
|
||||
return new Promise(resolve => {
|
||||
fetch(url).then(response => {
|
||||
const thumbnail = {
|
||||
frames: parseVtt(response),
|
||||
height: null,
|
||||
urlPrefix: '',
|
||||
};
|
||||
|
||||
// If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file
|
||||
// If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank
|
||||
// If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file
|
||||
if (
|
||||
!thumbnail.frames[0].text.startsWith('/') &&
|
||||
!thumbnail.frames[0].text.startsWith('http://') &&
|
||||
!thumbnail.frames[0].text.startsWith('https://')
|
||||
) {
|
||||
thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
// Download the first frame, so that we can determine/set the height of this thumbnailsDef
|
||||
const tempImage = new Image();
|
||||
|
||||
tempImage.onload = () => {
|
||||
thumbnail.height = tempImage.naturalHeight;
|
||||
thumbnail.width = tempImage.naturalWidth;
|
||||
|
||||
this.thumbnails.push(thumbnail);
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
startMove(event) {
|
||||
if (!this.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until media has a duration
|
||||
if (!this.player.media.duration) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'touchmove') {
|
||||
// Calculate seek hover position as approx video seconds
|
||||
this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);
|
||||
} else {
|
||||
// Calculate seek hover position as approx video seconds
|
||||
const clientRect = this.player.elements.progress.getBoundingClientRect();
|
||||
const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left);
|
||||
this.seekTime = this.player.media.duration * (percentage / 100);
|
||||
|
||||
if (this.seekTime < 0) {
|
||||
// The mousemove fires for 10+px out to the left
|
||||
this.seekTime = 0;
|
||||
}
|
||||
|
||||
if (this.seekTime > this.player.media.duration - 1) {
|
||||
// Took 1 second off the duration for safety, because different players can disagree on the real duration of a video
|
||||
this.seekTime = this.player.media.duration - 1;
|
||||
}
|
||||
|
||||
this.mousePosX = event.pageX;
|
||||
|
||||
// Set time text inside image container
|
||||
this.elements.thumb.time.innerText = formatTime(this.seekTime);
|
||||
}
|
||||
|
||||
// Download and show image
|
||||
this.showImageAtCurrentTime();
|
||||
}
|
||||
|
||||
endMove() {
|
||||
this.toggleThumbContainer(false, true);
|
||||
}
|
||||
|
||||
startScrubbing(event) {
|
||||
// Only act on left mouse button (0), or touch device (event.button is false)
|
||||
if (event.button === false || event.button === 0) {
|
||||
this.mouseDown = true;
|
||||
|
||||
// Wait until media has a duration
|
||||
if (this.player.media.duration) {
|
||||
this.toggleScrubbingContainer(true);
|
||||
this.toggleThumbContainer(false, true);
|
||||
|
||||
// Download and show image
|
||||
this.showImageAtCurrentTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endScrubbing() {
|
||||
this.mouseDown = false;
|
||||
|
||||
// Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview
|
||||
if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {
|
||||
// The video was already seeked/loaded at the chosen time - hide immediately
|
||||
this.toggleScrubbingContainer(false);
|
||||
} else {
|
||||
// The video hasn't seeked yet. Wait for that
|
||||
once.call(this.player, this.player.media, 'timeupdate', () => {
|
||||
// Re-check mousedown - we might have already started scrubbing again
|
||||
if (!this.mouseDown) {
|
||||
this.toggleScrubbingContainer(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup hooks for Plyr and window events
|
||||
*/
|
||||
listeners() {
|
||||
// Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering
|
||||
this.player.on('play', () => {
|
||||
this.toggleThumbContainer(false, true);
|
||||
});
|
||||
|
||||
this.player.on('seeked', () => {
|
||||
this.toggleThumbContainer(false);
|
||||
});
|
||||
|
||||
this.player.on('timeupdate', () => {
|
||||
this.lastTime = this.player.media.currentTime;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HTML elements for image containers
|
||||
*/
|
||||
render() {
|
||||
// Create HTML element: plyr__preview-thumbnail-container
|
||||
this.elements.thumb.container = createElement('div', {
|
||||
class: this.player.config.classNames.previewThumbnails.thumbContainer,
|
||||
});
|
||||
|
||||
// Wrapper for the image for styling
|
||||
this.elements.thumb.imageContainer = createElement('div', {
|
||||
class: this.player.config.classNames.previewThumbnails.imageContainer,
|
||||
});
|
||||
this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);
|
||||
|
||||
// Create HTML element, parent+span: time text (e.g., 01:32:00)
|
||||
const timeContainer = createElement('div', {
|
||||
class: this.player.config.classNames.previewThumbnails.timeContainer,
|
||||
});
|
||||
|
||||
this.elements.thumb.time = createElement('span', {}, '00:00');
|
||||
timeContainer.appendChild(this.elements.thumb.time);
|
||||
|
||||
this.elements.thumb.container.appendChild(timeContainer);
|
||||
|
||||
// Inject the whole thumb
|
||||
if (is.element(this.player.elements.progress)) {
|
||||
this.player.elements.progress.appendChild(this.elements.thumb.container);
|
||||
}
|
||||
|
||||
// Create HTML element: plyr__preview-scrubbing-container
|
||||
this.elements.scrubbing.container = createElement('div', {
|
||||
class: this.player.config.classNames.previewThumbnails.scrubbingContainer,
|
||||
});
|
||||
|
||||
this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);
|
||||
}
|
||||
|
||||
showImageAtCurrentTime() {
|
||||
if (this.mouseDown) {
|
||||
this.setScrubbingContainerSize();
|
||||
} else {
|
||||
this.setThumbContainerSizeAndPos();
|
||||
}
|
||||
|
||||
// Find the desired thumbnail index
|
||||
// TODO: Handle a video longer than the thumbs where thumbNum is null
|
||||
const thumbNum = this.thumbnails[0].frames.findIndex(
|
||||
frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,
|
||||
);
|
||||
const hasThumb = thumbNum >= 0;
|
||||
let qualityIndex = 0;
|
||||
|
||||
// Show the thumb container if we're not scrubbing
|
||||
if (!this.mouseDown) {
|
||||
this.toggleThumbContainer(hasThumb);
|
||||
}
|
||||
|
||||
// No matching thumb found
|
||||
if (!hasThumb) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if we've already downloaded higher quality versions of this image
|
||||
this.thumbnails.forEach((thumbnail, index) => {
|
||||
if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {
|
||||
qualityIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
// Only proceed if either thumbnum or thumbfilename has changed
|
||||
if (thumbNum !== this.showingThumb) {
|
||||
this.showingThumb = thumbNum;
|
||||
this.loadImage(qualityIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Show the image that's currently specified in this.showingThumb
|
||||
loadImage(qualityIndex = 0) {
|
||||
const thumbNum = this.showingThumb;
|
||||
const thumbnail = this.thumbnails[qualityIndex];
|
||||
const { urlPrefix } = thumbnail;
|
||||
const frame = thumbnail.frames[thumbNum];
|
||||
const thumbFilename = thumbnail.frames[thumbNum].text;
|
||||
const thumbUrl = urlPrefix + thumbFilename;
|
||||
|
||||
if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {
|
||||
// If we're already loading a previous image, remove its onload handler - we don't want it to load after this one
|
||||
// Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort
|
||||
if (this.loadingImage && this.usingSprites) {
|
||||
this.loadingImage.onload = null;
|
||||
}
|
||||
|
||||
// We're building and adding a new image. In other implementations of similar functionality (YouTube), background image
|
||||
// is instead used. But this causes issues with larger images in Firefox and Safari - switching between background
|
||||
// images causes a flicker. Putting a new image over the top does not
|
||||
const previewImage = new Image();
|
||||
previewImage.src = thumbUrl;
|
||||
previewImage.dataset.index = thumbNum;
|
||||
previewImage.dataset.filename = thumbFilename;
|
||||
this.showingThumbFilename = thumbFilename;
|
||||
|
||||
this.player.debug.log(`Loading image: ${thumbUrl}`);
|
||||
|
||||
// For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...
|
||||
previewImage.onload = () =>
|
||||
this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);
|
||||
this.loadingImage = previewImage;
|
||||
this.removeOldImages(previewImage);
|
||||
} else {
|
||||
// Update the existing image
|
||||
this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);
|
||||
this.currentImageElement.dataset.index = thumbNum;
|
||||
this.removeOldImages(this.currentImageElement);
|
||||
}
|
||||
}
|
||||
|
||||
showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) {
|
||||
this.player.debug.log(
|
||||
`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`,
|
||||
);
|
||||
this.setImageSizeAndOffset(previewImage, frame);
|
||||
|
||||
if (newImage) {
|
||||
this.currentImageContainer.appendChild(previewImage);
|
||||
this.currentImageElement = previewImage;
|
||||
|
||||
if (!this.loadedImages.includes(thumbFilename)) {
|
||||
this.loadedImages.push(thumbFilename);
|
||||
}
|
||||
}
|
||||
|
||||
// Preload images before and after the current one
|
||||
// Show higher quality of the same frame
|
||||
// Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading
|
||||
this.preloadNearby(thumbNum, true)
|
||||
.then(this.preloadNearby(thumbNum, false))
|
||||
.then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));
|
||||
}
|
||||
|
||||
// Remove all preview images that aren't the designated current image
|
||||
removeOldImages(currentImage) {
|
||||
// Get a list of all images, convert it from a DOM list to an array
|
||||
Array.from(this.currentImageContainer.children).forEach(image => {
|
||||
if (image.tagName.toLowerCase() !== 'img') {
|
||||
return;
|
||||
}
|
||||
|
||||
const removeDelay = this.usingSprites ? 500 : 1000;
|
||||
|
||||
if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {
|
||||
// Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients
|
||||
// First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function
|
||||
image.dataset.deleting = true;
|
||||
// This has to be set before the timeout - to prevent issues switching between hover and scrub
|
||||
const { currentImageContainer } = this;
|
||||
|
||||
setTimeout(() => {
|
||||
currentImageContainer.removeChild(image);
|
||||
this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);
|
||||
}, removeDelay);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Preload images before and after the current one. Only if the user is still hovering/seeking the same frame
|
||||
// This will only preload the lowest quality
|
||||
preloadNearby(thumbNum, forward = true) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;
|
||||
|
||||
if (this.showingThumbFilename === oldThumbFilename) {
|
||||
// Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away
|
||||
let thumbnailsClone;
|
||||
if (forward) {
|
||||
thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);
|
||||
} else {
|
||||
thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();
|
||||
}
|
||||
|
||||
let foundOne = false;
|
||||
|
||||
thumbnailsClone.forEach(frame => {
|
||||
const newThumbFilename = frame.text;
|
||||
|
||||
if (newThumbFilename !== oldThumbFilename) {
|
||||
// Found one with a different filename. Make sure it hasn't already been loaded on this page visit
|
||||
if (!this.loadedImages.includes(newThumbFilename)) {
|
||||
foundOne = true;
|
||||
this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);
|
||||
|
||||
const { urlPrefix } = this.thumbnails[0];
|
||||
const thumbURL = urlPrefix + newThumbFilename;
|
||||
|
||||
const previewImage = new Image();
|
||||
previewImage.src = thumbURL;
|
||||
previewImage.onload = () => {
|
||||
this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);
|
||||
if (!this.loadedImages.includes(newThumbFilename))
|
||||
this.loadedImages.push(newThumbFilename);
|
||||
|
||||
// We don't resolve until the thumb is loaded
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If there are none to preload then we want to resolve immediately
|
||||
if (!foundOne) {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
// If user has been hovering current image for half a second, look for a higher quality one
|
||||
getHigherQuality(currentQualityIndex, previewImage, frame, thumbFilename) {
|
||||
if (currentQualityIndex < this.thumbnails.length - 1) {
|
||||
// Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container
|
||||
let previewImageHeight = previewImage.naturalHeight;
|
||||
|
||||
if (this.usingSprites) {
|
||||
previewImageHeight = frame.h;
|
||||
}
|
||||
|
||||
if (previewImageHeight < this.thumbContainerHeight) {
|
||||
// Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while
|
||||
setTimeout(() => {
|
||||
// Make sure the mouse hasn't already moved on and started hovering at another image
|
||||
if (this.showingThumbFilename === thumbFilename) {
|
||||
this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);
|
||||
this.loadImage(currentQualityIndex + 1);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get currentImageContainer() {
|
||||
if (this.mouseDown) {
|
||||
return this.elements.scrubbing.container;
|
||||
}
|
||||
|
||||
return this.elements.thumb.imageContainer;
|
||||
}
|
||||
|
||||
get usingSprites() {
|
||||
return Object.keys(this.thumbnails[0].frames[0]).includes('w');
|
||||
}
|
||||
|
||||
get thumbAspectRatio() {
|
||||
if (this.usingSprites) {
|
||||
return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;
|
||||
}
|
||||
|
||||
return this.thumbnails[0].width / this.thumbnails[0].height;
|
||||
}
|
||||
|
||||
get thumbContainerHeight() {
|
||||
if (this.mouseDown) {
|
||||
// Can't use media.clientHeight - HTML5 video goes big and does black bars above and below
|
||||
return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio);
|
||||
}
|
||||
|
||||
return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);
|
||||
}
|
||||
|
||||
get currentImageElement() {
|
||||
if (this.mouseDown) {
|
||||
return this.currentScrubbingImageElement;
|
||||
}
|
||||
|
||||
return this.currentThumbnailImageElement;
|
||||
}
|
||||
|
||||
set currentImageElement(element) {
|
||||
if (this.mouseDown) {
|
||||
this.currentScrubbingImageElement = element;
|
||||
} else {
|
||||
this.currentThumbnailImageElement = element;
|
||||
}
|
||||
}
|
||||
|
||||
toggleThumbContainer(toggle = false, clearShowing = false) {
|
||||
const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;
|
||||
this.elements.thumb.container.classList.toggle(className, toggle);
|
||||
|
||||
if (!toggle && clearShowing) {
|
||||
this.showingThumb = null;
|
||||
this.showingThumbFilename = null;
|
||||
}
|
||||
}
|
||||
|
||||
toggleScrubbingContainer(toggle = false) {
|
||||
const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;
|
||||
this.elements.scrubbing.container.classList.toggle(className, toggle);
|
||||
|
||||
if (!toggle) {
|
||||
this.showingThumb = null;
|
||||
this.showingThumbFilename = null;
|
||||
}
|
||||
}
|
||||
|
||||
determineContainerAutoSizing() {
|
||||
if (this.elements.thumb.imageContainer.clientHeight > 20) {
|
||||
// This will prevent auto sizing in this.setThumbContainerSizeAndPos()
|
||||
this.sizeSpecifiedInCSS = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS
|
||||
setThumbContainerSizeAndPos() {
|
||||
if (!this.sizeSpecifiedInCSS) {
|
||||
const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);
|
||||
this.elements.thumb.imageContainer.style.height = `${this.thumbContainerHeight}px`;
|
||||
this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`;
|
||||
}
|
||||
|
||||
this.setThumbContainerPos();
|
||||
}
|
||||
|
||||
setThumbContainerPos() {
|
||||
const seekbarRect = this.player.elements.progress.getBoundingClientRect();
|
||||
const plyrRect = this.player.elements.container.getBoundingClientRect();
|
||||
const { container } = this.elements.thumb;
|
||||
|
||||
// Find the lowest and highest desired left-position, so we don't slide out the side of the video container
|
||||
const minVal = plyrRect.left - seekbarRect.left + 10;
|
||||
const maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10;
|
||||
|
||||
// Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth
|
||||
let previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2;
|
||||
|
||||
if (previewPos < minVal) {
|
||||
previewPos = minVal;
|
||||
}
|
||||
|
||||
if (previewPos > maxVal) {
|
||||
previewPos = maxVal;
|
||||
}
|
||||
|
||||
container.style.left = `${previewPos}px`;
|
||||
}
|
||||
|
||||
// Can't use 100% width, in case the video is a different aspect ratio to the video container
|
||||
setScrubbingContainerSize() {
|
||||
this.elements.scrubbing.container.style.width = `${this.player.media.clientWidth}px`;
|
||||
// Can't use media.clientHeight - html5 video goes big and does black bars above and below
|
||||
this.elements.scrubbing.container.style.height = `${this.player.media.clientWidth / this.thumbAspectRatio}px`;
|
||||
}
|
||||
|
||||
// Sprites need to be offset to the correct location
|
||||
setImageSizeAndOffset(previewImage, frame) {
|
||||
if (!this.usingSprites) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find difference between height and preview container height
|
||||
const multiplier = this.thumbContainerHeight / frame.h;
|
||||
|
||||
previewImage.style.height = `${Math.floor(previewImage.naturalHeight * multiplier)}px`;
|
||||
previewImage.style.width = `${Math.floor(previewImage.naturalWidth * multiplier)}px`;
|
||||
previewImage.style.left = `-${frame.x * multiplier}px`;
|
||||
previewImage.style.top = `-${frame.y * multiplier}px`;
|
||||
}
|
||||
}
|
||||
|
||||
export default PreviewThumbnails;
|
||||
@@ -9,386 +9,430 @@ import { createElement, replaceElement, toggleClass } from '../utils/elements';
|
||||
import { triggerEvent } from '../utils/events';
|
||||
import fetch from '../utils/fetch';
|
||||
import is from '../utils/is';
|
||||
import loadScript from '../utils/loadScript';
|
||||
import { extend } from '../utils/objects';
|
||||
import loadScript from '../utils/load-script';
|
||||
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() {
|
||||
// Add embed class for responsive
|
||||
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
|
||||
setup() {
|
||||
const player = this;
|
||||
|
||||
// Set intial ratio
|
||||
setAspectRatio.call(this);
|
||||
// Add embed class for responsive
|
||||
toggleClass(player.elements.wrapper, player.config.classNames.embed, true);
|
||||
|
||||
// Load the SDK if not already
|
||||
if (!is.object(window.Vimeo)) {
|
||||
loadScript(this.config.urls.vimeo.sdk)
|
||||
.then(() => {
|
||||
vimeo.ready.call(this);
|
||||
})
|
||||
.catch(error => {
|
||||
this.debug.warn('Vimeo SDK (player.js) failed to load', error);
|
||||
});
|
||||
} else {
|
||||
vimeo.ready.call(this);
|
||||
}
|
||||
},
|
||||
// Set speed options from config
|
||||
player.options.speed = player.config.speed.options;
|
||||
|
||||
// API Ready
|
||||
ready() {
|
||||
const player = this;
|
||||
const config = player.config.vimeo;
|
||||
// Set intial ratio
|
||||
setAspectRatio.call(player);
|
||||
|
||||
// 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,
|
||||
),
|
||||
);
|
||||
// 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);
|
||||
}
|
||||
},
|
||||
|
||||
// Get the source URL or ID
|
||||
let source = player.media.getAttribute('src');
|
||||
// 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 from <div> if needed
|
||||
if (is.empty(source)) {
|
||||
source = player.media.getAttribute(player.config.attributes.embed.id);
|
||||
// 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 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,
|
||||
});
|
||||
|
||||
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);
|
||||
// Set and show poster
|
||||
ui.setPoster.call(player, response.thumbnail_url).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
// 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');
|
||||
// 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;
|
||||
player.media.paused = true;
|
||||
player.media.currentTime = 0;
|
||||
|
||||
// Inject the package
|
||||
const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
|
||||
wrapper.appendChild(iframe);
|
||||
player.media = replaceElement(wrapper, player.media);
|
||||
// Disable native text track rendering
|
||||
if (player.supported.ui) {
|
||||
player.embed.disableTextTrack();
|
||||
}
|
||||
|
||||
// Get poster image
|
||||
fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
|
||||
if (is.empty(response)) {
|
||||
return;
|
||||
}
|
||||
// Create a faux HTML5 API using the Vimeo API
|
||||
player.media.play = () => {
|
||||
assurePlaybackState.call(player, true);
|
||||
return player.embed.play();
|
||||
};
|
||||
|
||||
// Get the URL for thumbnail
|
||||
const url = new URL(response[0].thumbnail_large);
|
||||
player.media.pause = () => {
|
||||
assurePlaybackState.call(player, false);
|
||||
return player.embed.pause();
|
||||
};
|
||||
|
||||
// Get original image
|
||||
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
|
||||
player.media.stop = () => {
|
||||
player.pause();
|
||||
player.currentTime = 0;
|
||||
};
|
||||
|
||||
// Set and show poster
|
||||
ui.setPoster.call(player, url.href).catch(() => {});
|
||||
});
|
||||
// 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
|
||||
|
||||
// Setup instance
|
||||
// https://github.com/vimeo/player.js
|
||||
player.embed = new window.Vimeo.Player(iframe, {
|
||||
autopause: player.config.autopause,
|
||||
muted: player.muted,
|
||||
});
|
||||
// Get current paused state and volume etc
|
||||
const { embed, media, paused, volume } = player;
|
||||
const restorePause = paused && !embed.hasPlayed;
|
||||
|
||||
player.media.paused = true;
|
||||
player.media.currentTime = 0;
|
||||
// Set seeking state and trigger event
|
||||
media.seeking = true;
|
||||
triggerEvent.call(player, media, 'seeking');
|
||||
|
||||
// Disable native text track rendering
|
||||
if (player.supported.ui) {
|
||||
player.embed.disableTextTrack();
|
||||
}
|
||||
// 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
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Create a faux HTML5 API using the Vimeo API
|
||||
player.media.play = () => {
|
||||
assurePlaybackState.call(player, true);
|
||||
return player.embed.play();
|
||||
};
|
||||
|
||||
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');
|
||||
})
|
||||
.catch(error => {
|
||||
// Hide menu item (and menu if empty)
|
||||
if (error.name === 'Error') {
|
||||
controls.setSpeedMenu.call(player, []);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 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('play', () => {
|
||||
assurePlaybackState.call(player, true);
|
||||
triggerEvent.call(player, player.media, 'playing');
|
||||
});
|
||||
player.embed.on('cuechange', ({ cues = [] }) => {
|
||||
const strippedCues = cues.map((cue) => stripHTML(cue.text));
|
||||
captions.updateCues.call(player, strippedCues);
|
||||
});
|
||||
|
||||
player.embed.on('pause', () => {
|
||||
assurePlaybackState.call(player, false);
|
||||
});
|
||||
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('timeupdate', data => {
|
||||
player.media.seeking = false;
|
||||
currentTime = data.seconds;
|
||||
triggerEvent.call(player, player.media, 'timeupdate');
|
||||
});
|
||||
if (is.element(player.embed.element) && player.supported.ui) {
|
||||
const frame = player.embed.element;
|
||||
|
||||
player.embed.on('progress', data => {
|
||||
player.media.buffered = data.percent;
|
||||
triggerEvent.call(player, player.media, 'progress');
|
||||
// Fix keyboard focus issues
|
||||
// https://github.com/sampotts/plyr/issues/317
|
||||
frame.setAttribute('tabindex', -1);
|
||||
}
|
||||
});
|
||||
|
||||
// Check all loaded
|
||||
if (parseInt(data.percent, 10) === 1) {
|
||||
triggerEvent.call(player, player.media, 'canplaythrough');
|
||||
}
|
||||
player.embed.on('bufferstart', () => {
|
||||
triggerEvent.call(player, player.media, 'waiting');
|
||||
});
|
||||
|
||||
// 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('bufferend', () => {
|
||||
triggerEvent.call(player, player.media, 'playing');
|
||||
});
|
||||
|
||||
player.embed.on('seeked', () => {
|
||||
player.media.seeking = false;
|
||||
triggerEvent.call(player, player.media, 'seeked');
|
||||
});
|
||||
player.embed.on('play', () => {
|
||||
assurePlaybackState.call(player, true);
|
||||
triggerEvent.call(player, player.media, 'playing');
|
||||
});
|
||||
|
||||
player.embed.on('ended', () => {
|
||||
player.media.paused = true;
|
||||
triggerEvent.call(player, player.media, 'ended');
|
||||
});
|
||||
player.embed.on('pause', () => {
|
||||
assurePlaybackState.call(player, false);
|
||||
});
|
||||
|
||||
player.embed.on('error', detail => {
|
||||
player.media.error = detail;
|
||||
triggerEvent.call(player, player.media, 'error');
|
||||
});
|
||||
player.embed.on('timeupdate', (data) => {
|
||||
player.media.seeking = false;
|
||||
currentTime = data.seconds;
|
||||
triggerEvent.call(player, player.media, 'timeupdate');
|
||||
});
|
||||
|
||||
// Rebuild UI
|
||||
setTimeout(() => ui.build.call(player), 0);
|
||||
},
|
||||
player.embed.on('progress', (data) => {
|
||||
player.media.buffered = data.percent;
|
||||
triggerEvent.call(player, player.media, 'progress');
|
||||
|
||||
// Check all loaded
|
||||
if (parseInt(data.percent, 10) === 1) {
|
||||
triggerEvent.call(player, player.media, 'canplaythrough');
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -7,432 +7,441 @@ import { createElement, replaceElement, toggleClass } from '../utils/elements';
|
||||
import { triggerEvent } from '../utils/events';
|
||||
import fetch from '../utils/fetch';
|
||||
import is from '../utils/is';
|
||||
import loadImage from '../utils/loadImage';
|
||||
import loadScript from '../utils/loadScript';
|
||||
import 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 {
|
||||
// Load the API
|
||||
loadScript(this.config.urls.youtube.sdk).catch(error => {
|
||||
this.debug.warn('YouTube API failed to load', error);
|
||||
});
|
||||
// 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 callback for the API
|
||||
// YouTube has it's own system of course...
|
||||
window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || [];
|
||||
|
||||
// Add to queue
|
||||
window.onYouTubeReadyCallbacks.push(() => {
|
||||
youtube.ready.call(this);
|
||||
});
|
||||
|
||||
// Set callback to process queue
|
||||
window.onYouTubeIframeAPIReady = () => {
|
||||
window.onYouTubeReadyCallbacks.forEach(callback => {
|
||||
callback();
|
||||
});
|
||||
};
|
||||
// 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;
|
||||
// Set aspect ratio
|
||||
this.embed.ratio = roundAspectRatio(width, height);
|
||||
}
|
||||
|
||||
// Ignore already setup (race condition)
|
||||
const currentId = player.media.getAttribute('id');
|
||||
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
|
||||
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);
|
||||
player.media.pause = () => {
|
||||
assurePlaybackState.call(player, false);
|
||||
instance.pauseVideo();
|
||||
};
|
||||
|
||||
// Get poster, if already set
|
||||
const { poster } = player;
|
||||
player.media.stop = () => {
|
||||
instance.stopVideo();
|
||||
};
|
||||
|
||||
// Replace media element
|
||||
const container = createElement('div', { id, poster });
|
||||
player.media = replaceElement(container, player.media);
|
||||
player.media.duration = instance.getDuration();
|
||||
player.media.paused = true;
|
||||
|
||||
// Id to poster wrapper
|
||||
const posterSrc = format => `https://i.ytimg.com/vi/${videoId}/${format}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(posterSrc => {
|
||||
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
|
||||
if (!posterSrc.includes('maxres')) {
|
||||
player.elements.poster.style.backgroundSize = 'cover';
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
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
|
||||
player.options.speed = instance.getAvailablePlaybackRates();
|
||||
|
||||
// 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;
|
||||
|
||||
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;
|
||||
|
||||
711
src/js/plyr.d.ts
vendored
Normal file
711
src/js/plyr.d.ts
vendored
Normal file
@@ -0,0 +1,711 @@
|
||||
// Type definitions for plyr 3.5
|
||||
// Project: https://plyr.io
|
||||
// Definitions by: ondratra <https://github.com/ondratra>
|
||||
// TypeScript Version: 3.0
|
||||
|
||||
export = Plyr;
|
||||
export as namespace Plyr;
|
||||
|
||||
declare class Plyr {
|
||||
/**
|
||||
* Setup a new instance
|
||||
*/
|
||||
static setup(targets: NodeList | HTMLElement | HTMLElement[] | string, options?: Plyr.Options): Plyr[];
|
||||
|
||||
/**
|
||||
* Check for support
|
||||
* @param mediaType
|
||||
* @param provider
|
||||
* @param playsInline Whether the player has the playsinline attribute (only applicable to iOS 10+)
|
||||
*/
|
||||
static supported(mediaType?: Plyr.MediaType, provider?: Plyr.Provider, playsInline?: boolean): Plyr.Support;
|
||||
|
||||
constructor(targets: NodeList | HTMLElement | HTMLElement[] | string, options?: Plyr.Options);
|
||||
|
||||
/**
|
||||
* Indicates if the current player is HTML5.
|
||||
*/
|
||||
readonly isHTML5: boolean;
|
||||
|
||||
/**
|
||||
* Indicates if the current player is an embedded player.
|
||||
*/
|
||||
readonly isEmbed: boolean;
|
||||
|
||||
/**
|
||||
* Indicates if the current player is playing.
|
||||
*/
|
||||
readonly playing: boolean;
|
||||
|
||||
/**
|
||||
* Indicates if the current player is paused.
|
||||
*/
|
||||
readonly paused: boolean;
|
||||
|
||||
/**
|
||||
* Indicates if the current player is stopped.
|
||||
*/
|
||||
readonly stopped: boolean;
|
||||
|
||||
/**
|
||||
* Indicates if the current player has finished playback.
|
||||
*/
|
||||
readonly ended: boolean;
|
||||
|
||||
/**
|
||||
* Returns a float between 0 and 1 indicating how much of the media is buffered
|
||||
*/
|
||||
readonly buffered: number;
|
||||
|
||||
/**
|
||||
* Gets or sets the currentTime for the player. The setter accepts a float in seconds.
|
||||
*/
|
||||
currentTime: number;
|
||||
|
||||
/**
|
||||
* Indicates if the current player is seeking.
|
||||
*/
|
||||
readonly seeking: boolean;
|
||||
|
||||
/**
|
||||
* Returns the duration for the current media.
|
||||
*/
|
||||
readonly duration: number;
|
||||
|
||||
/**
|
||||
* Gets or sets the volume for the player. The setter accepts a float between 0 and 1.
|
||||
*/
|
||||
volume: number;
|
||||
|
||||
/**
|
||||
* Gets or sets the muted state of the player. The setter accepts a boolean.
|
||||
*/
|
||||
muted: boolean;
|
||||
|
||||
/**
|
||||
* Indicates if the current media has an audio track.
|
||||
*/
|
||||
readonly hasAudio: boolean;
|
||||
|
||||
/**
|
||||
* Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5.
|
||||
*/
|
||||
speed: number;
|
||||
|
||||
/**
|
||||
* Gets or sets the quality for the player. The setter accepts a value from the options specified in your config.
|
||||
*/
|
||||
quality: number;
|
||||
|
||||
/**
|
||||
* Gets or sets the current loop state of the player.
|
||||
*/
|
||||
loop: boolean;
|
||||
|
||||
/**
|
||||
* Gets or sets the current source for the player.
|
||||
*/
|
||||
source: Plyr.SourceInfo;
|
||||
|
||||
/**
|
||||
* Gets or sets the current poster image URL for the player.
|
||||
*/
|
||||
poster: string;
|
||||
|
||||
/**
|
||||
* Gets or sets the autoplay state of the player.
|
||||
*/
|
||||
autoplay: boolean;
|
||||
|
||||
/**
|
||||
* Gets or sets the caption track by index. 1 means the track is missing or captions is not active
|
||||
*/
|
||||
currentTrack: number;
|
||||
|
||||
/**
|
||||
* Gets or sets the preferred captions language for the player. The setter accepts an ISO twoletter 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.
|
||||
*/
|
||||
language: string;
|
||||
|
||||
/**
|
||||
* Gets or sets the picture-in-picture state of the player. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+.
|
||||
*/
|
||||
pip: boolean;
|
||||
|
||||
/**
|
||||
* Gets or sets the aspect ratio for embedded players.
|
||||
*/
|
||||
ratio?: string;
|
||||
|
||||
/**
|
||||
* Access Elements cache
|
||||
*/
|
||||
elements: Plyr.Elements;
|
||||
|
||||
/**
|
||||
* Returns the current video Provider
|
||||
*/
|
||||
readonly provider: Plyr.Provider;
|
||||
|
||||
/**
|
||||
* Returns the native API for Vimeo or Youtube players
|
||||
*/
|
||||
readonly embed?: any;
|
||||
|
||||
readonly fullscreen: Plyr.FullscreenControl;
|
||||
|
||||
/**
|
||||
* Start playback.
|
||||
* For HTML5 players, play() will return a Promise in some browsers - WebKit and Mozilla according to MDN at time of writing.
|
||||
*/
|
||||
play(): Promise<void> | void;
|
||||
|
||||
/**
|
||||
* Pause playback.
|
||||
*/
|
||||
pause(): void;
|
||||
|
||||
/**
|
||||
* Toggle playback, if no parameters are passed, it will toggle based on current status.
|
||||
*/
|
||||
togglePlay(toggle?: boolean): boolean;
|
||||
|
||||
/**
|
||||
* Stop playback and reset to start.
|
||||
*/
|
||||
stop(): void;
|
||||
|
||||
/**
|
||||
* Restart playback.
|
||||
*/
|
||||
restart(): void;
|
||||
|
||||
/**
|
||||
* Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used.
|
||||
*/
|
||||
rewind(seekTime?: number): void;
|
||||
|
||||
/**
|
||||
* Fast forward by the specified seek time. If no parameter is passed, the default seek time will be used.
|
||||
*/
|
||||
forward(seekTime?: number): void;
|
||||
|
||||
/**
|
||||
* Increase volume by the specified step. If no parameter is passed, the default step will be used.
|
||||
*/
|
||||
increaseVolume(step?: number): void;
|
||||
|
||||
/**
|
||||
* Increase volume by the specified step. If no parameter is passed, the default step will be used.
|
||||
*/
|
||||
decreaseVolume(step?: number): void;
|
||||
|
||||
/**
|
||||
* Toggle captions display. If no parameter is passed, it will toggle based on current status.
|
||||
*/
|
||||
toggleCaptions(toggle?: boolean): void;
|
||||
|
||||
/**
|
||||
* Trigger the airplay dialog on supported devices.
|
||||
*/
|
||||
airplay(): void;
|
||||
|
||||
/**
|
||||
* Sets the preview thubmnails for the current source.
|
||||
*/
|
||||
setPreviewThumbnails(source: Plyr.PreviewThumbnailsOptions): void;
|
||||
|
||||
/**
|
||||
* Toggle the controls (video only). Takes optional truthy value to force it on/off.
|
||||
*/
|
||||
toggleControls(toggle: boolean): void;
|
||||
|
||||
/**
|
||||
* Add an event listener for the specified event.
|
||||
*/
|
||||
on<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
|
||||
|
||||
/**
|
||||
* Add an event listener for the specified event once.
|
||||
*/
|
||||
once<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
|
||||
|
||||
/**
|
||||
* Remove an event listener for the specified event.
|
||||
*/
|
||||
off<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
|
||||
|
||||
/**
|
||||
* Check support for a mime type.
|
||||
*/
|
||||
supports(type: string): boolean;
|
||||
|
||||
/**
|
||||
* Destroy lib instance
|
||||
* @param {Function} callback - Callback for when destroy is complete
|
||||
* @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)
|
||||
*/
|
||||
destroy(callback?: (...args: any[]) => void, soft?: boolean): void;
|
||||
}
|
||||
|
||||
declare namespace Plyr {
|
||||
type MediaType = 'audio' | 'video';
|
||||
type Provider = 'html5' | 'youtube' | 'vimeo';
|
||||
type StandardEventMap = {
|
||||
progress: PlyrEvent;
|
||||
playing: PlyrEvent;
|
||||
play: PlyrEvent;
|
||||
pause: PlyrEvent;
|
||||
timeupdate: PlyrEvent;
|
||||
volumechange: PlyrEvent;
|
||||
seeking: PlyrEvent;
|
||||
seeked: PlyrEvent;
|
||||
ratechange: PlyrEvent;
|
||||
ended: PlyrEvent;
|
||||
enterfullscreen: PlyrEvent;
|
||||
exitfullscreen: PlyrEvent;
|
||||
captionsenabled: PlyrEvent;
|
||||
captionsdisabled: PlyrEvent;
|
||||
languagechange: PlyrEvent;
|
||||
controlshidden: PlyrEvent;
|
||||
controlsshown: PlyrEvent;
|
||||
ready: PlyrEvent;
|
||||
};
|
||||
// For retrocompatibility, we keep StandadEvent
|
||||
type StandadEvent = keyof Plyr.StandardEventMap;
|
||||
type Html5EventMap = {
|
||||
loadstart: PlyrEvent;
|
||||
loadeddata: PlyrEvent;
|
||||
loadedmetadata: PlyrEvent;
|
||||
canplay: PlyrEvent;
|
||||
canplaythrough: PlyrEvent;
|
||||
stalled: PlyrEvent;
|
||||
waiting: PlyrEvent;
|
||||
emptied: PlyrEvent;
|
||||
cuechange: PlyrEvent;
|
||||
error: PlyrEvent;
|
||||
};
|
||||
// For retrocompatibility, we keep Html5Event
|
||||
type Html5Event = keyof Plyr.Html5EventMap;
|
||||
type YoutubeEventMap = {
|
||||
statechange: PlyrStateChangeEvent;
|
||||
qualitychange: PlyrEvent;
|
||||
qualityrequested: PlyrEvent;
|
||||
};
|
||||
// For retrocompatibility, we keep YoutubeEvent
|
||||
type YoutubeEvent = keyof Plyr.YoutubeEventMap;
|
||||
|
||||
type PlyrEventMap = StandardEventMap & Html5EventMap & YoutubeEventMap;
|
||||
|
||||
interface FullscreenControl {
|
||||
/**
|
||||
* Indicates if the current player is in fullscreen mode.
|
||||
*/
|
||||
readonly active: boolean;
|
||||
|
||||
/**
|
||||
* Indicates if the current player has fullscreen enabled.
|
||||
*/
|
||||
readonly enabled: boolean;
|
||||
|
||||
/**
|
||||
* Enter fullscreen. If fullscreen is not supported, a fallback ""full window/viewport"" is used instead.
|
||||
*/
|
||||
enter(): void;
|
||||
|
||||
/**
|
||||
* Exit fullscreen.
|
||||
*/
|
||||
exit(): void;
|
||||
|
||||
/**
|
||||
* Toggle fullscreen.
|
||||
*/
|
||||
toggle(): void;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
enabled?: boolean;
|
||||
|
||||
/**
|
||||
* Display debugging information in the console
|
||||
*/
|
||||
debug?: boolean;
|
||||
|
||||
/**
|
||||
* 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 for more info on how the html needs to be structured.
|
||||
* Defaults to ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']
|
||||
*/
|
||||
controls?: string[] | ((id: string, seektime: number, title: string) => unknown) | Element;
|
||||
|
||||
/**
|
||||
* If you're using the default controls are used then you can specify which settings to show in the menu
|
||||
* Defaults to ['captions', 'quality', 'speed', 'loop']
|
||||
*/
|
||||
settings?: string[];
|
||||
|
||||
/**
|
||||
* Used for internationalization (i18n) of the text within the UI.
|
||||
*/
|
||||
i18n?: any;
|
||||
|
||||
/**
|
||||
* Load the SVG sprite specified as the iconUrl option (if a URL). If false, it is assumed you are handling sprite loading yourself.
|
||||
*/
|
||||
loadSprite?: boolean;
|
||||
|
||||
/**
|
||||
* Specify a URL or path to the SVG sprite. See the SVG section for more info.
|
||||
*/
|
||||
iconUrl?: string;
|
||||
|
||||
/**
|
||||
* Specify the id prefix for the icons used in the default controls (e.g. plyr-play would be plyr).
|
||||
* This is to prevent clashes if you're using your own SVG sprite but with the default controls.
|
||||
* Most people can ignore this option.
|
||||
*/
|
||||
iconPrefix?: string;
|
||||
|
||||
/**
|
||||
* Specify a URL or path to a blank video file used to properly cancel network requests.
|
||||
*/
|
||||
blankVideo?: string;
|
||||
|
||||
/**
|
||||
* Autoplay the media on load. This is generally advised against on UX grounds. It is also disabled by default in some browsers.
|
||||
* If the autoplay attribute is present on a <video> or <audio> element, this will be automatically set to true.
|
||||
*/
|
||||
autoplay?: boolean;
|
||||
|
||||
/**
|
||||
* Only allow one player playing at once.
|
||||
*/
|
||||
autopause?: boolean;
|
||||
|
||||
/**
|
||||
* The time, in seconds, to seek when a user hits fast forward or rewind.
|
||||
*/
|
||||
seekTime?: number;
|
||||
|
||||
/**
|
||||
* A number, between 0 and 1, representing the initial volume of the player.
|
||||
*/
|
||||
volume?: number;
|
||||
|
||||
/**
|
||||
* Whether to start playback muted. If the muted attribute is present on a <video> or <audio> element, this will be automatically set to true.
|
||||
*/
|
||||
muted?: boolean;
|
||||
|
||||
/**
|
||||
* Click (or tap) of the video container will toggle play/pause.
|
||||
*/
|
||||
clickToPlay?: boolean;
|
||||
|
||||
/**
|
||||
* Disable right click menu on video to help as very primitive obfuscation to prevent downloads of content.
|
||||
*/
|
||||
disableContextMenu?: boolean;
|
||||
|
||||
/**
|
||||
* Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen.
|
||||
* As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly.
|
||||
*/
|
||||
hideControls?: boolean;
|
||||
|
||||
/**
|
||||
* Reset the playback to the start once playback is complete.
|
||||
*/
|
||||
resetOnEnd?: boolean;
|
||||
|
||||
/**
|
||||
* Enable keyboard shortcuts for focused players only or globally
|
||||
*/
|
||||
keyboard?: KeyboardOptions;
|
||||
|
||||
/**
|
||||
* controls: Display control labels as tooltips on :hover & :focus (by default, the labels are screen reader only).
|
||||
* seek: Display a seek tooltip to indicate on click where the media would seek to.
|
||||
*/
|
||||
tooltips?: TooltipOptions;
|
||||
|
||||
/**
|
||||
* Specify a custom duration for media.
|
||||
*/
|
||||
duration?: number;
|
||||
|
||||
/**
|
||||
* Displays the duration of the media on the metadataloaded event (on startup) in the current time display.
|
||||
* This will only work if the preload attribute is not set to none (or is not set at all) and you choose not to display the duration (see controls option).
|
||||
*/
|
||||
displayDuration?: boolean;
|
||||
|
||||
/**
|
||||
* Display the current time as a countdown rather than an incremental counter.
|
||||
*/
|
||||
invertTime?: boolean;
|
||||
|
||||
/**
|
||||
* Allow users to click to toggle the above.
|
||||
*/
|
||||
toggleInvert?: boolean;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
listeners?: {[key: string]: (error: PlyrEvent) => void};
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
captions?: CaptionOptions;
|
||||
|
||||
/**
|
||||
* enabled: Toggles whether fullscreen should be enabled. fallback: Allow fallback to a full-window solution.
|
||||
* iosNative: whether to use native iOS fullscreen when entering fullscreen (no custom controls)
|
||||
*/
|
||||
fullscreen?: FullScreenOptions;
|
||||
|
||||
/**
|
||||
* The aspect ratio you want to use for embedded players.
|
||||
*/
|
||||
ratio?: string;
|
||||
|
||||
/**
|
||||
* enabled: Allow use of local storage to store user settings. key: The key name to use.
|
||||
*/
|
||||
storage?: StorageOptions;
|
||||
|
||||
/**
|
||||
* 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?: SpeedOptions;
|
||||
|
||||
/**
|
||||
* Currently only supported by YouTube. default is the default quality level, determined by YouTube. options are the options to display.
|
||||
*/
|
||||
quality?: QualityOptions;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
loop?: LoopOptions;
|
||||
|
||||
/**
|
||||
* enabled: Whether to enable vi.ai ads. publisherId: Your unique vi.ai publisher ID.
|
||||
*/
|
||||
ads?: AdOptions;
|
||||
|
||||
/**
|
||||
* Vimeo Player Options.
|
||||
*/
|
||||
vimeo?: object;
|
||||
|
||||
/**
|
||||
* Youtube Player Options.
|
||||
*/
|
||||
youtube?: object;
|
||||
|
||||
/**
|
||||
* Preview Thumbnails Options.
|
||||
*/
|
||||
previewThumbnails?: PreviewThumbnailsOptions;
|
||||
|
||||
/**
|
||||
* Media Metadata Options.
|
||||
*/
|
||||
mediaMetadata?: MediaMetadataOptions;
|
||||
}
|
||||
|
||||
interface QualityOptions {
|
||||
default: number;
|
||||
forced?: boolean;
|
||||
onChange?: (quality: number) => void;
|
||||
options: number[];
|
||||
}
|
||||
|
||||
interface LoopOptions {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface AdOptions {
|
||||
enabled: boolean;
|
||||
publisherId?: string;
|
||||
tagUrl?: string;
|
||||
}
|
||||
|
||||
interface SpeedOptions {
|
||||
selected: number;
|
||||
options: number[];
|
||||
}
|
||||
|
||||
interface KeyboardOptions {
|
||||
focused?: boolean;
|
||||
global?: boolean;
|
||||
}
|
||||
|
||||
interface TooltipOptions {
|
||||
controls?: boolean;
|
||||
seek?: boolean;
|
||||
}
|
||||
|
||||
interface FullScreenOptions {
|
||||
enabled?: boolean;
|
||||
fallback?: boolean | 'force';
|
||||
allowAudio?: boolean;
|
||||
iosNative?: boolean;
|
||||
container?: string;
|
||||
}
|
||||
|
||||
interface CaptionOptions {
|
||||
active?: boolean;
|
||||
language?: string;
|
||||
update?: boolean;
|
||||
}
|
||||
|
||||
interface StorageOptions {
|
||||
enabled?: boolean;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
interface PreviewThumbnailsOptions {
|
||||
enabled?: boolean;
|
||||
src?: string | string[];
|
||||
}
|
||||
|
||||
interface MediaMetadataArtwork {
|
||||
src: string;
|
||||
sizes?: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface MediaMetadataOptions {
|
||||
title?: string;
|
||||
artist?: string;
|
||||
album?: string;
|
||||
artwork?: MediaMetadataArtwork[];
|
||||
}
|
||||
|
||||
export interface Elements {
|
||||
buttons: {
|
||||
airplay?: HTMLButtonElement;
|
||||
captions?: HTMLButtonElement;
|
||||
download?: HTMLButtonElement;
|
||||
fastForward?: HTMLButtonElement;
|
||||
fullscreen?: HTMLButtonElement;
|
||||
mute?: HTMLButtonElement;
|
||||
pip?: HTMLButtonElement;
|
||||
play?: HTMLButtonElement | HTMLButtonElement[];
|
||||
restart?: HTMLButtonElement;
|
||||
rewind?: HTMLButtonElement;
|
||||
settings?: HTMLButtonElement;
|
||||
};
|
||||
captions: HTMLElement | null;
|
||||
container: HTMLElement | null;
|
||||
controls: HTMLElement | null;
|
||||
fullscreen: HTMLElement | null;
|
||||
wrapper: HTMLElement | null;
|
||||
}
|
||||
|
||||
interface SourceInfo {
|
||||
/**
|
||||
* Note: YouTube and Vimeo are currently not supported as audio sources.
|
||||
*/
|
||||
type: MediaType;
|
||||
|
||||
/**
|
||||
* Title of the new media. Used for the aria-label attribute on the play button, and outer container. YouTube and Vimeo are populated automatically.
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* This is an array of sources. For HTML5 media, the properties of this object are mapped directly to HTML attributes so more can be added to the object if required.
|
||||
*/
|
||||
sources: Source[];
|
||||
|
||||
/**
|
||||
* The URL for the poster image (HTML5 video only).
|
||||
*/
|
||||
poster?: string;
|
||||
|
||||
/**
|
||||
* An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above,
|
||||
* it will render as <track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default> and similar for the French version.
|
||||
* Booleans are converted to HTML5 value-less attributes.
|
||||
*/
|
||||
tracks?: Track[];
|
||||
|
||||
/**
|
||||
* Enable or disable preview thumbnails for current source
|
||||
*/
|
||||
previewThumbnails?: Plyr.PreviewThumbnailsOptions;
|
||||
}
|
||||
|
||||
interface Source {
|
||||
/**
|
||||
* The URL of the media file (or YouTube/Vimeo URL).
|
||||
*/
|
||||
src: string;
|
||||
/**
|
||||
* The MIME type of the media file (if HTML5).
|
||||
*/
|
||||
type?: string;
|
||||
provider?: Provider;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
type TrackKind = 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
|
||||
interface Track {
|
||||
/**
|
||||
* Indicates how the text track is meant to be used
|
||||
*/
|
||||
kind: TrackKind;
|
||||
/**
|
||||
* Indicates a user-readable title for the track
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* The language of the track text data. It must be a valid BCP 47 language tag. If the kind attribute is set to subtitles, then srclang must be defined.
|
||||
*/
|
||||
srcLang?: string;
|
||||
/**
|
||||
* The URL of the track (.vtt file).
|
||||
*/
|
||||
src: string;
|
||||
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
interface PlyrEvent extends CustomEvent {
|
||||
readonly detail: {readonly plyr: Plyr};
|
||||
}
|
||||
|
||||
enum YoutubeState {
|
||||
UNSTARTED = -1,
|
||||
ENDED = 0,
|
||||
PLAYING = 1,
|
||||
PAUSED = 2,
|
||||
BUFFERING = 3,
|
||||
CUED = 5,
|
||||
}
|
||||
|
||||
interface PlyrStateChangeEvent extends CustomEvent {
|
||||
readonly detail: {
|
||||
readonly plyr: Plyr;
|
||||
readonly code: YoutubeState;
|
||||
};
|
||||
}
|
||||
|
||||
interface Support {
|
||||
api: boolean;
|
||||
ui: boolean;
|
||||
}
|
||||
}
|
||||
2162
src/js/plyr.js
2162
src/js/plyr.js
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,13 @@
|
||||
// ==========================================================================
|
||||
// Plyr Polyfilled Build
|
||||
// plyr.js v3.5.4
|
||||
// plyr.js v3.7.0
|
||||
// https://github.com/sampotts/plyr
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
|
||||
import 'custom-event-polyfill';
|
||||
import 'url-polyfill';
|
||||
|
||||
import Plyr from './plyr';
|
||||
|
||||
export default Plyr;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user