Compare commits

...

155 Commits

Author SHA1 Message Date
Sam Potts bb7f7d5e2a 3.5.7 2020-02-09 21:59:40 +00:00
Sam Potts 8c44425665 Merge pull request #1679 from sampotts/develop
3.5.7
2020-02-09 21:53:24 +00:00
Sam Potts 93e3f8946a Docs updates 2020-02-09 21:52:34 +00:00
Sam Potts 95431639a0 Merge branch 'develop' of github.com:sampotts/plyr into develop 2020-02-09 21:42:54 +00:00
Sam Potts 3e3186cfeb Update docs for speed options tweaks 2020-02-09 21:42:27 +00:00
Sam Potts 2d13ad3d39 Focus trap improvements 2020-02-09 21:42:12 +00:00
Sam Potts 74ba6a96fc Set download attribute for HTML5 only 2020-02-09 10:36:32 +00:00
Sam Potts e1cb2f24f5 Merge pull request #1490 from antonyoneill/develop
Prevent default on settings control click
2020-02-09 10:30:33 +00:00
Sam Potts 59e3ef7248 Comments 2020-02-09 10:20:40 +00:00
Sam Potts 0b1c480729 Package upgrades 2020-02-08 23:09:50 +00:00
Sam Potts 90dc985657 Clean up speed options logic 2020-02-08 23:09:41 +00:00
Sam Potts b5456e1de7 Merge pull request #1671 from ydylla/improve-speed-options
Use the configured speed options
2020-02-08 22:29:49 +00:00
Sam Potts 976eebc2a2 Merge pull request #1672 from ydylla/improve-quality-change
Improve/fix quality change state restoring
2020-02-08 22:19:44 +00:00
Sam Potts f00c279366 Merge pull request #1675 from Laerdal/focus-trap-only-fullscreen
Trap keyboard focus only when fullscreen
2020-02-08 22:18:59 +00:00
Sam Potts b651d6f027 Merge pull request #1676 from Code1110/develop
Add download attribute to download button
2020-02-08 22:18:14 +00:00
Sam Potts b63b62f6dc Merge pull request #1677 from ydylla/fix-scrubbing-chrome-android
Fix preview thumbnail scrubbing not working on mobile touch devices
2020-02-08 22:17:41 +00:00
Sam Potts 0f08c7c13a Ignore quality change if it matches existing 2020-02-08 21:48:51 +00:00
ydylla 9075ea189a fix scrubbing for chrome android & hide thumb preview on touchend
Chrome android sends TouchEvent which does not have a button property.
2020-02-08 19:14:06 +01:00
Code1110 c33f0995f9 add download attribute to download button 2020-02-07 18:15:56 +01:00
Kimberley Jensen e17da7dfd4 Bail out of focus trap if fullscreen is not active
- detailed in https://github.com/sampotts/plyr/issues/1665
2020-02-07 15:00:04 +01:00
ydylla 299f712cc9 actually use the configured speed options 2020-02-01 16:37:38 +01:00
ydylla f755a3c401 preserve playback rate at quality change 2020-02-01 16:35:24 +01:00
ydylla 472bb479d4 fix regression: not restoring playback state after quality change 2020-02-01 16:35:17 +01:00
Sam Potts 8b9521d5a5 Fix beta deployment 2020-01-30 14:57:14 +00:00
Sam Potts 818e1efd43 Deployed 3.5.7-beta.0 2020-01-30 14:56:52 +00:00
Sam Potts 58f5380694 Merge pull request #1662 from sampotts/develop
3.5.7
2020-01-30 14:23:40 +00:00
Sam Potts 9d51291125 Merge pull request #1663 from sampotts/master
Merge back to beta
2020-01-30 14:23:10 +00:00
Sam Potts fefcca7805 Prepare for 3.5.7 release 2020-01-30 11:34:07 +00:00
Sam Potts 5204f33d45 Package upgrades 2020-01-30 11:07:14 +00:00
Sam Potts 0041ea3050 Merge branch 'develop' of github.com:sampotts/plyr into develop 2020-01-26 22:05:29 +00:00
Sam Potts f8a28b632c Audio style fix 2020-01-26 22:05:21 +00:00
Sam Potts 9c817cff68 Merge pull request #1653 from thatrobotdev/patch-2
Patch 2
2020-01-22 11:32:37 +00:00
Sam Potts 521e8abf88 Merge pull request #1654 from thatrobotdev/patch-3
Fix broken link to new plugin
2020-01-22 11:31:02 +00:00
Sam Potts 9fe03c7474 Merge pull request #1655 from laukstein/patch-1
Uncaught RangeError: Maximum call stack size exceeded
2020-01-22 11:30:19 +00:00
Binyamin Laukstein c3f10e7df3 Uncaught RangeError: Maximum call stack size exceeded
Fix formatTime infinite loop #1621
2020-01-22 08:53:08 +02:00
James Kerrane 1b8c51f45e Fix broken link to new plugin
Fix broken link, and update to an active project replacement https://github.com/chintan9/plyr-react.
2020-01-21 23:15:18 -07:00
James Kerrane 1bb4c207f1 Change vimeo demo video
Change vimeo video to a more general video, fix #1626.
2020-01-21 23:08:55 -07:00
Sam Potts b6da2702a2 Fix reference 2020-01-21 22:30:58 +00:00
Sam Potts fb704b945d Presentation fixes 2020-01-21 22:29:00 +00:00
Sam Potts 71d6f59d56 HTML5 poster fixes for multiple downloads 2020-01-21 22:28:48 +00:00
Sam Potts 89136bc2e6 Merge branch 'develop' of github.com:sampotts/plyr into develop
# Conflicts:
#	src/sass/components/video.scss
2020-01-21 22:15:38 +00:00
Sam Potts b6d94e000f Merge pull request #1651 from shravan2x/develop
Fixed Plyr container not resizing responsively
2020-01-21 22:11:32 +00:00
Shravan Rajinikanth bfcb7133cb Fixed Plyr container not resizing responsively 2020-01-19 06:05:12 -08:00
Sam Potts 7883792ccc Fix issue with browser sync preview 2020-01-14 22:33:01 +00:00
Sam Potts def3668030 Fix issues with fixed ratios and 100% height/width 2020-01-14 22:32:45 +00:00
Sam Potts a77d2d56f6 Fix build 2020-01-14 07:50:48 +00:00
Sam Potts 7ee041403f Merge branch 'develop' of github.com:sampotts/plyr into develop
# Conflicts:
#	src/js/controls.js
#	src/sass/components/volume.scss
2020-01-14 07:46:48 +00:00
Sam Potts 53a400027f Remove logic to hide/show volume controls based on audio track 2020-01-14 07:45:24 +00:00
Sam Potts a2498acf7c Manually merge PR #1629 2020-01-14 07:44:59 +00:00
Sam Potts 28a1098c0f Merge pull request #1629 from sumanbh/ios-show-volume
IOS - Fix being unable to unmute auto-played videos
2020-01-14 07:31:34 +00:00
Sam Potts 6ffaef35cf Manually merged PR #1607 2020-01-14 07:25:41 +00:00
Sam Potts ff105ee203 Fix browser sync vs watch issues 2020-01-14 07:25:04 +00:00
Sam Potts 56c0d7bd4d Fix linting issues 2020-01-13 16:38:12 +00:00
Sam Potts d00d31961e Merge pull request #1484 from MaxGiting/patch-1
Improve clarity
2020-01-13 16:32:43 +00:00
Sam Potts b2d3ef5f38 Merge pull request #1505 from nskazki/detach-event-listeners-on-destroy
Detach event listeners on destroy
2020-01-13 16:31:17 +00:00
Sam Potts b2ac730572 Merge pull request #1535 from skerbis/master
adds: REDAXO CMS Plyr AddOn
2020-01-13 16:29:41 +00:00
Sam Potts 3424d08d3a Merge pull request #1521 from ondratra/develop
typescript typings
2020-01-13 16:29:15 +00:00
Sam Potts 5dd9462bed Merge pull request #1516 from azizhk/toggle_return_promsie
Toggle also returns promise
2020-01-13 16:25:17 +00:00
Sam Potts 402eb2b761 Merge pull request #1552 from SoftCreatR/patch-2
Fix ads configuration
2020-01-13 16:24:25 +00:00
Sam Potts 63d74eee68 Merge pull request #1560 from 0xflotus/develop
Update readme.md
2020-01-13 16:23:39 +00:00
Sam Potts 166a27d094 Comment clean up 2020-01-13 16:22:49 +00:00
Sam Potts 4f06e2eb71 Merge pull request #1570 from felipedeboni/ie11-resetonend-fix
Prevents IE11 with resetOnEnd option set to true to play video again
2020-01-13 16:21:46 +00:00
Sam Potts 0b240ae7d1 Fix linting issues 2020-01-13 16:20:02 +00:00
Sam Potts 6b0e5cd6f1 Merge branch 'master' into develop 2020-01-13 16:18:38 +00:00
Sam Potts 2463434d27 Merge branch 'develop' of github.com:sampotts/plyr into develop 2020-01-13 16:18:24 +00:00
Sam Potts c09b9ac01c Manually port over change from PR #1616 2020-01-13 16:18:05 +00:00
Sam Potts 15a1cdde89 Merge pull request #1565 from lmislm/patch-1
Update defaults.js
2020-01-13 16:16:35 +00:00
Sam Potts 3a1da5ad36 Merge pull request #1525 from lunika/play-button-aria
️(controls) change play button aria-label value when its state change
2020-01-13 16:09:04 +00:00
Sam Potts 67cb324aed Merge pull request #1577 from avidnewmedia/issue-615
#615: updates to vimeo and youtube buffering state
2020-01-13 16:06:27 +00:00
Sam Potts 8736fa8a52 Manually port over change from PR #1625 2020-01-13 16:04:27 +00:00
Sam Potts cb4dab4250 Merge pull request #1579 from taion/fix-listener-return
fix: Fix handling listener return value
2020-01-13 15:59:36 +00:00
Sam Potts c56916a8e0 Merge pull request #1582 from bseib/1306-preserve-svg-symbol-viewbox
Preserve viewBox attribute in SVG sprite symbols
2020-01-13 15:58:27 +00:00
Sam Potts 93963d3915 Merge pull request #1630 from pjbaert/patch-1
Typo in readme
2020-01-13 15:56:27 +00:00
Sam Potts e7c6f965b4 Merge branch 'master' into develop 2020-01-13 15:49:47 +00:00
Sam Potts 4f263ebb1a Added local server, package upgrades 2020-01-13 15:49:29 +00:00
Sam Potts c15bdabf0c Update readme.md 2020-01-12 21:35:40 +00:00
Pieter-Jan Baert 80a077c50a Typo in readme 2019-12-06 16:00:48 +01:00
Suman Bhattarai 42d72c5303 fix being unable to unmute autoplayed video on IOS 2019-12-03 10:34:27 -07:00
Sam Potts 74e3990604 Merge pull request #1590 from ayunami2000/patch-1
typo
2019-10-23 13:01:56 +11:00
ayunami2000 e6e391ad6a typo
change s to z in "Customisable" (now "Customizable")
2019-10-14 15:59:01 -04:00
Broc Seib 12e5099c92 Preserve viewBox attribute in SVG sprite symbols
When generating the SVG sprite (using imagemin and svstore) the imagemin
step needs to NOT cleanup/remove the viewBox attributes from the
individual svg files.

fixes #1306
2019-10-01 17:34:49 -04:00
Jimmy Jia 72afffbc8d fix: Fix handling listener return value 2019-09-25 14:21:13 -04:00
Dustin Harrell 627df20b6d #615: updates to vimeo and youtube plugins to ensure that loading classes are added as content is buffering 2019-09-23 18:26:49 -04:00
Felipe K. De Boni bb43ef15fe Prevents IE11 with resetOnEnd option set to true to play video again 2019-09-16 11:53:07 -03:00
Baskerville* 7907652bc9 Update defaults.js
update defaults.i18n
2019-09-06 16:18:40 +08:00
0xflotus dce665b792 Update readme.md 2019-09-01 18:39:37 +02:00
Sascha Greuel e978bc8690 Fixed ads configuration 2019-08-27 19:28:51 +02:00
Thomas Skerbis 91503d3698 Update readme.md 2019-08-15 21:55:22 +02:00
Thomas Skerbis 1721f6e9e2 Update readme.md 2019-08-15 21:53:41 +02:00
Manuel Raynaud c7b5aa9197 ️(controls) change play button aria-label value when its state change
The aria-label attribute set on all play buttons does not change
according the player state. When the video is playing, the aria-label
should change to pause otherwise screen reader will not detect that this
button now can be used to pause the video.
2019-08-07 18:00:02 +02:00
ondratra 800c8e0a17 typescript typings 2019-08-07 00:08:10 +02:00
Aziz Khambati d771da9abf Toggle also returns promise 2019-07-30 18:19:10 +05:30
nskazki b36b92b247 Detach event listeners on destroy
if "on" receives "this", it attaches "eventListeners" array to "this" and stores "{ element, type, callback }" hash in there.
later, during the destruction process, all the entries from this array will be processed by "unbindListeners" helper to detach all the event listeners.
2019-07-19 16:26:01 +03:00
Sam Potts aa51719a55 Merge pull request #1497 from ffpetrovic/patch-1
Update index.html
2019-07-12 08:46:42 +10:00
Filip Petrovic a28685536a Update index.html 2019-07-11 23:18:35 +02:00
Antony O'Neill 400fd77d0a Prevent default on settings icon click 2019-07-04 19:02:22 +01:00
MaxGiting a2bf974058 Improve clarity 2019-07-01 15:09:29 +01:00
Sam Potts 7c442c9357 3.5.6 2019-06-21 12:35:47 +10:00
Sam Potts e17e0a81dd Package upgrade 2019-06-21 12:35:22 +10:00
Sam Potts 2488299d7b Edge fix 2019-06-21 12:34:49 +10:00
Sam Potts dfc09b8e04 v3.5.5 deployed 2019-06-21 00:24:28 +10:00
Sam Potts 6438baaddc Merge branch 'develop' 2019-06-21 00:19:51 +10:00
Sam Potts 8fc6c2ba52 File rename and clean up 2019-06-21 00:19:37 +10:00
Sam Potts 95092edc93 Merge pull request #1472 from sampotts/develop
v3.5.5
2019-06-21 00:12:10 +10:00
Sam Potts c4b3e0672e Clean up 2019-06-21 00:10:57 +10:00
Sam Potts e8e2b8ba39 Merge branch 'master' into develop
# Conflicts:
#	.eslintrc
#	demo/dist/demo.css
#	demo/dist/demo.js
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.css
#	dist/plyr.js
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.min.mjs
#	dist/plyr.min.mjs.map
#	dist/plyr.mjs
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	dist/plyr.polyfilled.min.mjs
#	dist/plyr.polyfilled.min.mjs.map
#	dist/plyr.polyfilled.mjs
#	package.json
#	readme.md
#	src/js/listeners.js
#	yarn.lock
2019-06-20 23:56:19 +10:00
Sam Potts 2e40b91ec1 Styling tweaks for demo 2019-06-20 23:50:46 +10:00
Sam Potts 97d9228bed Aspect ratio tweaks 2019-06-03 20:13:16 +10:00
Sam Potts 0f14865d56 Add duration (commented out) in defaults 2019-06-03 20:12:43 +10:00
Sam Potts c94ab2a39f Repaint clean up 2019-06-03 20:12:21 +10:00
Sam Potts ab89e055de Demo tweaks 2019-06-03 20:11:31 +10:00
Sam Potts ac6e3dba5a Fix for thumbnails in demo for audio 2019-06-03 00:28:09 +10:00
Sam Potts d9d2c4a219 Demo tweaks 2019-06-03 00:26:08 +10:00
Sam Potts 1890a9378d Gulp tweaks 2019-06-02 23:16:45 +10:00
Sam Potts ac88e6e190 Demo clean up 2019-06-02 23:16:29 +10:00
Sam Potts 15cbae8a19 Removed commented out code for Edge 2019-06-02 22:25:44 +10:00
Sam Potts aaf096d96d Removed raven-js from dependencies 2019-06-02 22:25:17 +10:00
Sam Potts 93f5acbffd Fixed cite display 2019-06-02 22:25:00 +10:00
Sam Potts 9c717275d2 Packages for demo separated 2019-06-02 22:24:41 +10:00
Sam Potts 0249772f01 Clean up 2019-06-01 19:50:29 +10:00
Sam Potts 5d699d5f47 Merge branch 'develop' of github.com:sampotts/plyr into develop 2019-06-01 19:29:21 +10:00
Sam Potts 34d79a5443 Merge pull request #1453 from aFarkas/develop
youtube multiple small issues
2019-06-01 18:46:56 +10:00
Sam Potts 1e761e237a Merge pull request #1458 from Jason-Cooke/patch-1
docs: fix typo
2019-06-01 18:46:14 +10:00
Sam Potts 36ad132c82 Edge fix 2019-06-01 18:45:12 +10:00
Sam Potts c9055f391b Linting changes 2019-06-01 18:45:07 +10:00
Jason Cooke 99c95363b4 docs: fix typo 2019-06-01 11:13:44 +12:00
Sam Potts c90526bf07 Create FUNDING.yml 2019-05-27 17:17:05 +10:00
Alexander Farkas 97a6e72e10 fix youtube embed + handle early destroy 2019-05-24 16:55:45 +02:00
Alexander Farkas f100caba81 fix youtube embed + handle early destroy 2019-05-24 16:53:58 +02:00
Sam Potts 80aa6ffe43 Linting changes 2019-04-30 23:44:05 +10:00
Sam Potts 0694e58650 v3.5.4 2019-04-25 12:14:48 +10:00
Sam Potts e644eeb5b6 Merge pull request #1423 from sampotts/develop
v3.5.4
2019-04-25 12:11:06 +10:00
Sam Potts 5ddd9e02de Fix for the menu in the shadow DOM 2019-04-25 12:09:46 +10:00
Sam Potts a064f0b4fd Merge branch 'develop' of github.com:sampotts/plyr into develop 2019-04-25 12:04:38 +10:00
Sam Potts b9dbcc30fa Eslint config rename 2019-04-25 12:03:42 +10:00
Sam Potts 7ab34c7d2e Changelog 2019-04-25 12:03:32 +10:00
Sam Potts 17caa3c57b Fix merging class 2019-04-25 12:03:24 +10:00
Sam Potts 2e6361898b Package updates 2019-04-25 12:03:13 +10:00
Sam Potts a1b2c0419f Ads improvements for volume and race condition fix 2019-04-25 12:03:04 +10:00
Sam Potts 9b81e776fb Styling for control changes 2019-04-25 12:02:48 +10:00
Sam Potts 7a4a7dece8 Ratio improvements 2019-04-25 12:02:37 +10:00
Sam Potts f0d3e8c3b9 Fix for className being wiped out 2019-04-25 12:01:52 +10:00
Sam Potts ad68d9484f Clean up and API change 2019-04-25 12:01:30 +10:00
Sam Potts c3fd822857 Notes on autoplay support 2019-04-25 12:01:15 +10:00
Sam Potts e147f3a754 Formatting 2019-04-25 12:01:00 +10:00
Sam Potts b694e7d3ab Use polyfill.io v3 2019-04-22 12:28:26 +10:00
Sam Potts b2fff4c33f Increase speed limits 2019-04-15 22:08:09 +10:00
Sam Potts 9c1060d9b0 Merge pull request #1416 from emielbeinema/fix_webcomponents
Make menu work in WebComponent
2019-04-15 21:41:23 +10:00
Emiel Beinema a91652287b Don't close menu on click in menu in webcomponent 2019-04-15 12:17:10 +02:00
Emiel Beinema 2cf44c236d Use querySelector on container for showMenuPanel 2019-04-15 12:16:48 +02:00
Sam Potts 243db9eda3 Fix issue with empty controls and preview thumbs 2019-04-13 12:56:15 +10:00
Sam Potts b675ba1f35 Fix issue with setGutter call 2019-04-13 12:55:48 +10:00
Sam Potts e9367ee85e Fix setting initial speed (fixes #1408) 2019-04-12 20:18:17 +10:00
Sam Potts d9b7928ce6 Merge branch 'master' into develop 2019-04-12 19:00:23 +10:00
Sam Potts cdacae6697 Set download URL via setter 2019-04-12 19:00:17 +10:00
98 changed files with 28761 additions and 11755 deletions
+30
View File
@@ -0,0 +1,30 @@
{
"parser": "babel-eslint",
"extends": ["airbnb-base", "prettier"],
"plugins": ["simple-import-sort", "import"],
"env": {
"browser": true,
"es6": true
},
"globals": {
"Plyr": false,
"jQuery": false
},
"rules": {
"import/no-cycle": "warn",
"padding-line-between-statements": [
"error",
{
"blankLine": "never",
"prev": ["singleline-const", "singleline-let", "singleline-var"],
"next": ["singleline-const", "singleline-let", "singleline-var"]
}
],
"sort-imports": "off",
"import/order": "off",
"simple-import-sort/sort": "error"
},
"parserOptions": {
"sourceType": "module"
}
}
-35
View File
@@ -1,35 +0,0 @@
{
"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"
}
}
+4
View File
@@ -0,0 +1,4 @@
# These are supported funding model platforms
github: sampotts
patreon: plyr
-2
View File
@@ -3,9 +3,7 @@ node_modules
credentials.json
*.mp4
!dist/blank.mp4
index-*.html
npm-debug.log
yarn-error.log
package-lock.json
*.webm
.idea/
+4 -4
View File
@@ -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"
]
}
+4 -2
View File
@@ -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": {
+55 -3
View File
@@ -1,4 +1,56 @@
## v3.5.3
### v3.5.7
- Typescript typings (thanks @ondratra)
- `togglePlay` now also returns a `Promise` (thanks @azizhk)
- Documentation improvements and typo fixes (thanks @ffpetrovic, @skerbis, @ayunami2000, @pjbaert, @MaxGiting, @0xflotus and @thatrobotdev)
- Accessibility tweak for the play button (thanks @lunika)
- Fix for ads configuration (thanks @SoftCreatR)
- Fix handling listener return value (thanks @taion)
- Added localisation key for PIP (picture-in-picture) (thanks @lmislm)
- Preserve viewBox attribute in SVG sprite symbols (thanks @bseib)
- Fix being unable to unmute autoplayed video on iOS (thanks @sumanbh)
- Fixed Plyr container not resizing responsively (thanks @shravan2x)
- Change vimeo demo video (thanks @thatrobotdev)
- Fix for `Uncaught RangeError: Maximum call stack size exceeded` (thanks @laukstein)
- Improve fullscreen experience on some devices (thanks @savroff)
- Improvements to buffering state for embedded players (thanks @doostinharrell)
- Prevents IE11 with resetOnEnd option set to true to play video again (thanks @Felipe K. De Boni)
- Fix for multiple poster image downloads (use the native poster only for HTML5 videos)
- Various presentational fixes
- Removed logic to hide/show volume controls based on audio track detection due to it's problematic nature. If you want to hide volume control, use the `controls` option to do so.
- Fix preview thumbnail scrubbing not working on mobile touch devices (thanks @ydylla)
- Add download attribute to download button (thanks @Code1110)
- Trap keyboard focus only when fullscreen (thanks @k-jensen)
- Improvements to speed options - you can now specify all options in the UI (YouTube and Vimeo only accept 0.5-2) (thanks @ydylla)
- Improve/fix quality change state restoring (thanks @ydylla)
_Note:_ This update contains CSS changes.
### v3.5.6
- Another Edge fix (thanks Nick Hawk via Slack)
### v3.5.5
- YouTube fix for when there are other embeds on the page (thanks @aFarkas)
- Separated demo dependencies into their own package.json
- Fix for Edge controls flexbox issue when resizing the player (thanks Nick Hawk via Slack)
- More aspect ratio fixes
### v3.5.4
- Added: Set download URL via new setter
- Improvement: The order of the `controls` option now effects the order in the DOM - i.e. you can re-order the controls - Note: this may break any custom CSS you have setup. Please see the changes in the PR to the default SASS
- Fixed issue with empty controls and preview thumbs
- Fixed issue with setGutter call (from Sentry)
- Fixed issue with initial selected speed not working
- Added notes on `autoplay` config option and browser compatibility
- Fixed issue with ads volume not matching current content volume
- Fixed race condition where ads were loading during source change
- Improvement: Automatic aspect ratio for YouTube is now supported, meaning all aspect ratios are set based on media content - Note: we're now using a different API to get YouTube video metadata so you may need to adjust any CSPs you have setup
- Fix for menu in the Shadow DOM (thanks @emielbeinema)
### v3.5.3
- Improved the usage of the `ratio` config option; it now works as expected and for all video types. The default has not changed, it is to dynamically, where possible (except YouTube where 16:9 is used) determine the ratio from the media source so this is not a breaking change.
- Added new `ratio` getter and setter
@@ -6,11 +58,11 @@
- Fix: Allow absolute paths in preview thumbnails
- Improvement: Allow optional hours and ms in VTT parser in preview thumbnails
## v3.5.2
### v3.5.2
- Fixed issue where the preview thumbnail was present while scrubbing
## v3.5.1
### v3.5.1
- Fixed build issues with babel and browserslist
+1 -1
View File
File diff suppressed because one or more lines are too long
+14386 -1627
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

+1 -1
View File
File diff suppressed because one or more lines are too long
+29 -30
View File
@@ -17,7 +17,7 @@
<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 -->
<!-- Open Graph -->
<meta
property="og:title"
content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player"
@@ -50,12 +50,23 @@
type="font/woff2"
href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2"
/>
<!-- Google Analytics-->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-132699580-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-132699580-1');
</script>
</head>
<body>
<div class="grid">
<header>
<h1>Plyr</h1>
<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">
@@ -98,7 +109,7 @@
</p>
<p>
Premium video monitization from
Premium video monetization from
<a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border">
<img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi" />
<span class="sr-only">ai.vi</span>
@@ -106,22 +117,20 @@
</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
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>
<a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr">
<svg class="icon" role="presentation">
<title>GitHub</title>
<path
d="M8,0.2c-4.4,0-8,3.6-8,8c0,3.5,2.3,6.5,5.5,7.6
C5.9,15.9,6,15.6,6,15.4c0-0.2,0-0.7,0-1.4C3.8,14.5,3.3,13,3.3,13c-0.4-0.9-0.9-1.2-0.9-1.2c-0.7-0.5,0.1-0.5,0.1-0.5
c0.8,0.1,1.2,0.8,1.2,0.8C4.4,13.4,5.6,13,6,12.8c0.1-0.5,0.3-0.9,0.5-1.1c-1.8-0.2-3.6-0.9-3.6-4c0-0.9,0.3-1.6,0.8-2.1
c-0.1-0.2-0.4-1,0.1-2.1c0,0,0.7-0.2,2.2,0.8c0.6-0.2,1.3-0.3,2-0.3c0.7,0,1.4,0.1,2,0.3c1.5-1,2.2-0.8,2.2-0.8
c0.4,1.1,0.2,1.9,0.1,2.1c0.5,0.6,0.8,1.3,0.8,2.1c0,3.1-1.9,3.7-3.7,3.9C9.7,12,10,12.5,10,13.2c0,1.1,0,1.9,0,2.2
c0,0.2,0.1,0.5,0.6,0.4c3.2-1.1,5.5-4.1,5.5-7.6C16,3.8,12.4,0.2,8,0.2z"
></path>
</svg>
Download on GitHub
</a>
</div>
</header>
@@ -256,23 +265,13 @@
<a
href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts"
target="_blank"
class="js-shr-button"
class="js-shr"
>tweet it</a
>
👍
</p>
</aside>
<!-- Polyfills -->
<script
src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL,Math.trunc"
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>
</html>
+14
View 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": {
"core-js": "^3.1.4",
"custom-event-polyfill": "^1.0.7",
"raven-js": "^3.27.2",
"shr-buttons": "2.0.3",
"url-polyfill": "^1.1.5"
}
}
+63 -191
View File
@@ -4,8 +4,16 @@
// Please see readme.md in the root or github.com/sampotts/plyr
// ==========================================================================
import './tab-focus';
import 'custom-event-polyfill';
import 'url-polyfill';
import Raven from 'raven-js';
import Shr from 'shr-buttons';
import Plyr from '../../../src/js/plyr';
import sources from './sources';
import toggleClass from './toggle-class';
(() => {
const { host } = window.location;
@@ -17,45 +25,15 @@ import Plyr from '../../../src/js/plyr';
document.addEventListener('DOMContentLoaded', () => {
Raven.context(() => {
const selector = '#player';
const container = document.getElementById('container');
if (window.Shr) {
window.Shr.setup('.js-shr-button', {
count: {
classname: 'button__count',
},
});
}
// 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 share buttons
Shr.setup('.js-shr', {
count: {
className: 'button__count',
},
wrapper: {
className: 'button--with-count',
},
});
// Setup the player
@@ -93,131 +71,12 @@ import Plyr from '../../../src/js/plyr';
// 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 &ndash; &ldquo;It All Began With A Burst&rdquo;',
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;
const types = Object.keys(sources);
const historySupport = Boolean(window.history && window.history.pushState);
let currentType = window.location.hash.substring(1);
const hasCurrentType = !currentType.length;
function render(type) {
// Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
@@ -226,9 +85,31 @@ import Plyr from '../../../src/js/plyr';
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
cite.setAttribute('hidden', '');
// eslint-disable-next-line no-param-reassign
cite.hidden = true;
});
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
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
@@ -236,7 +117,7 @@ import Plyr from '../../../src/js/plyr';
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
newSource(type);
setSource(type);
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
@@ -246,36 +127,27 @@ import Plyr from '../../../src/js/plyr';
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && 'type' in event.state) {
newSource(event.state.type);
if (event.state && Object.keys(event.state).includes('type')) {
setSource(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);
}
// If there's no current type set, assume video
if (hasCurrentType) {
currentType = 'video';
}
// Replace current history state
if (historySupport && types.includes(currentType)) {
window.history.replaceState({ type: currentType }, '', hasCurrentType ? '' : `#${currentType}`);
}
// If it's not video, load the source
if (currentType !== 'video') {
setSource(currentType, true);
}
render(currentType);
});
});
+78
View File
@@ -0,0 +1,78 @@
const sources = {
video: {
type: 'video',
title: 'View From A Blue Moon',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
type: 'video/mp4',
size: 576,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
type: 'video/mp4',
size: 720,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
type: 'video/mp4',
size: 1080,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
type: 'video/mp4',
size: 1440,
},
],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
},
audio: {
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
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/383514704',
provider: 'vimeo',
},
],
},
};
export default sources;
+31
View 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
View 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;
+10 -9
View File
@@ -6,11 +6,9 @@
.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;
@@ -21,14 +19,16 @@
// Buttons
.button {
background: $color-button-background;
color: $color-button-text;
font-weight: $font-weight-bold;
padding-left: $spacing-base;
padding-right: $spacing-base;
padding-left: ($spacing-base * 1.25);
padding-right: ($spacing-base * 1.25);
transition: all 0.2s ease;
&:hover,
&:focus {
color: $gray-dark;
background: $color-button-background-hover;
// Remove the underline/border
&::after {
@@ -38,7 +38,6 @@
&:hover {
box-shadow: 0 2px 2px rgba(#000, 0.1);
transform: translateY(-1px);
}
&:focus {
@@ -50,7 +49,7 @@
}
&:active {
transform: translateY(1px);
top: 1px;
}
}
@@ -66,12 +65,14 @@
// Count bubble
.button__count {
animation: fadein 0.2s ease;
margin-left: ($spacing-base / 2);
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;
border-right-color: $color-button-count-background;
content: '';
height: 0;
position: absolute;
+11
View File
@@ -6,6 +6,13 @@ header {
padding-bottom: $spacing-base;
text-align: center;
h1 span {
animation: shrinkHide 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) 2s forwards;
display: inline-block;
font-weight: $font-weight-light;
opacity: 0.5;
}
.call-to-action {
margin-top: ($spacing-base * 1.5);
}
@@ -15,5 +22,9 @@ header {
max-width: 360px;
padding-bottom: ($spacing-base * 2);
text-align: left;
p:first-of-type {
@include font-size($font-size-base + 1);
}
}
}
+1 -1
View File
@@ -19,5 +19,5 @@ label svg {
a .icon,
.btn .icon {
margin-right: floor($spacing-base / 3);
margin-right: ($spacing-base / 2);
}
-1
View File
@@ -12,7 +12,6 @@ button.faux-link {
a {
border-bottom: 1px dotted currentColor;
color: $color-link;
font-weight: $font-weight-bold;
position: relative;
text-decoration: none;
transition: all 0.2s ease;
+2 -16
View File
@@ -2,16 +2,10 @@
// Examples
// ==========================================================================
// For non supported browsers
video {
max-width: 100%;
vertical-align: middle;
}
// Example players
.plyr {
border-radius: $border-radius-base;
box-shadow: 0 2px 5px rgba(#000, 0.2);
box-shadow: 0 2px 15px rgba(#000, 0.1);
margin: $spacing-base auto;
&.plyr--audio {
@@ -34,17 +28,9 @@ video {
// Style full supported player
.plyr__cite {
display: none;
margin-top: $spacing-base;
color: $color-gray-5;
.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;
}
+1 -2
View File
@@ -35,11 +35,10 @@ main {
aside {
align-items: center;
background: #fff;
color: $gray;
display: flex;
flex-shrink: 0;
justify-content: center;
padding: ($spacing-base * 0.75);
padding: $spacing-base;
position: relative;
text-align: center;
text-shadow: none;
+14
View File
@@ -11,3 +11,17 @@
opacity: 1;
}
}
@keyframes shrinkHide {
0% {
opacity: 0.5;
width: 38px;
}
20% {
width: 45px;
}
100% {
opacity: 0;
width: 0;
}
}
+1 -1
View File
@@ -36,7 +36,7 @@
@return #{$rem}rem;
}
@mixin font-size($size: 16) {
@mixin font-size($size: $font-size-base) {
font-size: $size * 1px; // Fallback in px
font-size: calculate-rem($size);
}
+26 -16
View File
@@ -2,31 +2,41 @@
// Colors
// ==========================================================================
// Greyscale
$gray-dark: #343f4a;
$gray: #55646b;
$gray-light: #cbd0d3;
$gray-lighter: #dbe3e8;
$off-white: #f2f5f7;
// Grayscale
$color-gray-9: hsl(210, 15%, 16%);
$color-gray-8: lighten($color-gray-9, 9%);
$color-gray-7: lighten($color-gray-8, 9%);
$color-gray-6: lighten($color-gray-7, 9%);
$color-gray-5: lighten($color-gray-6, 9%);
$color-gray-4: lighten($color-gray-5, 9%);
$color-gray-3: lighten($color-gray-4, 9%);
$color-gray-2: lighten($color-gray-3, 9%);
$color-gray-1: lighten($color-gray-2, 9%);
$color-gray-0: lighten($color-gray-1, 9%);
// Branding
$color-brand-primary: hsl(198, 100%, 50%);
// Text
$color-text: #fff;
// Plyr
$color-brand-primary: #1aafff;
$color-text: $color-gray-7;
$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(198, 100%, 94%);
$color-background-to: hsl(198, 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(198, 100%, 55%);
$color-button-count-background: #fff;
$color-button-count-text: $color-gray-6;
// Focus
$tab-focus-default-color: #fff;
+1 -1
View File
@@ -9,4 +9,4 @@ $arrow-size: 5px;
$border-radius-base: 4px;
// 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);
+3
View File
@@ -14,6 +14,9 @@ $plyr-font-size-badges: 9px;
// Other
$plyr-font-smoothing: true;
// Colors
$plyr-color-main: $color-brand-primary;
// Captions
$plyr-font-size-captions-base: $plyr-font-size-base;
$plyr-font-size-captions-small: $plyr-font-size-small;
+1 -1
View File
@@ -2,4 +2,4 @@
// Colors
// ==========================================================================
$spacing-base: 20px;
$spacing-base: 16px;
+1 -2
View File
@@ -14,7 +14,6 @@ body {
font-family: $font-sans-serif;
font-weight: $font-weight-medium;
line-height: $line-height-base;
text-shadow: 0 1px 1px rgba(#000, 0.15);
}
button,
@@ -26,7 +25,7 @@ textarea {
p,
small {
margin: 0 0 $spacing-base;
margin: 0 0 ($spacing-base * 1.5);
}
small {
+2 -1
View File
@@ -4,8 +4,9 @@
h1 {
@include font-size($font-size-h1);
color: $color-headings;
font-weight: $font-weight-bold;
letter-spacing: $letter-spacing-headings;
line-height: 1.2;
margin: 0 0 $spacing-base;
margin: 0 0 ($spacing-base * 1.5);
}
+28
View File
@@ -0,0 +1,28 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
core-js@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.4.tgz#3a2837fc48e582e1ae25907afcd6cf03b0cc7a07"
integrity sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==
custom-event-polyfill@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee"
integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
raven-js@^3.27.2:
version "3.27.2"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.27.2.tgz#6c33df952026cd73820aa999122b7b7737a66775"
integrity sha512-mFWQcXnhRFEQe5HeFroPaEghlnqy7F5E2J3Fsab189ondqUzcjwSVi7el7F36cr6PvQYXoZ1P2F5CSF2/azeMQ==
shr-buttons@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/shr-buttons/-/shr-buttons-2.0.3.tgz#2ffd021fc3d789e1510ce2736b938bd09ea1da5a"
integrity sha512-sPAgHiw4uaIt9TnxTfyZEedDChcldSVtnBHE44cpe/mSC7rqm4IEKZRLYqnVlTcGM+FSDNBPUNpSf50Q2ntd+w==
url-polyfill@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.5.tgz#bec79b72b5407dba6d8cced2e32e4ab273aa9fb1"
integrity sha512-9XjIJ6nwrU+nGd8t90Ze0Zs7t8A+SU0gqsqPttj6j3zAVe5q0HFcuv37nDBdVSPpi4aTHTfbUF/i+ZVD+o2EbA==
+1 -1
View File
File diff suppressed because one or more lines are too long
+1114 -924
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1112 -922
View File
File diff suppressed because it is too large Load Diff
+3154 -2361
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+3152 -2359
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

+64 -24
View File
@@ -1,29 +1,32 @@
// ==========================================================================
// Gulp build script
// ==========================================================================
/* global require, __dirname */
/* eslint no-console: "off" */
const path = require('path');
const gulp = require('gulp');
// ------------------------------------
// JavaScript
// ------------------------------------
const terser = require('gulp-terser');
const rollup = require('gulp-better-rollup');
const babel = require('rollup-plugin-babel');
const commonjs = require('rollup-plugin-commonjs');
const resolve = require('rollup-plugin-node-resolve');
// ------------------------------------
// CSS
// ------------------------------------
const sass = require('gulp-sass');
const clean = require('gulp-clean-css');
const prefix = require('gulp-autoprefixer');
// ------------------------------------
// Images
// ------------------------------------
const svgstore = require('gulp-svgstore');
const imagemin = require('gulp-imagemin');
// ------------------------------------
// Utils
// ------------------------------------
const del = require('del');
const filter = require('gulp-filter');
const header = require('gulp-header');
@@ -37,18 +40,23 @@ const plumber = require('gulp-plumber');
const size = require('gulp-size');
const sourcemaps = require('gulp-sourcemaps');
const through = require('through2');
const browserSync = require('browser-sync').create();
// ------------------------------------
// Deployment
// ------------------------------------
const aws = require('aws-sdk');
const publish = require('gulp-awspublish');
const FastlyPurge = require('fastly-purge');
// ------------------------------------
// Configs
// ------------------------------------
const pkg = require('./package.json');
const build = require('./build.json');
const deploy = require('./deploy.json');
// ------------------------------------
// Info from package
// ------------------------------------
const { browserslist: browsers, version } = pkg;
const minSuffix = '.min';
// Get AWS config
@@ -125,15 +133,16 @@ gulp.task(tasks.clean, done => {
// JavaScript
Object.entries(build.js).forEach(([filename, entry]) => {
entry.formats.forEach(format => {
const { dist, formats, namespace, polyfill, src } = entry;
formats.forEach(format => {
const name = `js:${filename}:${format}`;
tasks.js.push(name);
const polyfill = filename.includes('polyfilled');
const extension = format === 'es' ? 'mjs' : 'js';
tasks.js.push(name);
gulp.task(name, () =>
gulp
.src(entry.src)
.src(src)
.pipe(plumber())
.pipe(sourcemaps.init())
.pipe(
@@ -159,7 +168,7 @@ Object.entries(build.js).forEach(([filename, entry]) => {
],
},
{
name: entry.namespace,
name: namespace,
format,
},
),
@@ -170,25 +179,26 @@ Object.entries(build.js).forEach(([filename, entry]) => {
extname: `.${extension}`,
}),
)
.pipe(gulp.dest(entry.dist))
.pipe(gulp.dest(dist))
.pipe(filter(`**/*.${extension}`))
.pipe(terser())
.pipe(rename({ suffix: minSuffix }))
.pipe(size(sizeOptions))
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(entry.dist)),
.pipe(gulp.dest(dist)),
);
});
});
// CSS
Object.entries(build.css).forEach(([filename, entry]) => {
const { dist, src } = entry;
const name = `css:${filename}`;
tasks.css.push(name);
gulp.task(name, () =>
gulp
.src(entry.src)
.src(src)
.pipe(plumber())
.pipe(sass())
.pipe(
@@ -198,24 +208,31 @@ Object.entries(build.css).forEach(([filename, entry]) => {
)
.pipe(clean())
.pipe(size(sizeOptions))
.pipe(gulp.dest(entry.dist)),
.pipe(gulp.dest(dist)),
);
});
// SVG Sprites
Object.entries(build.sprite).forEach(([filename, entry]) => {
const { dist, src } = entry;
const name = `sprite:${filename}`;
tasks.sprite.push(name);
gulp.task(name, () =>
gulp
.src(entry.src)
.src(src)
.pipe(plumber())
.pipe(imagemin())
.pipe(
imagemin([
imagemin.svgo({
plugins: [{ removeViewBox: false }],
}),
]),
)
.pipe(svgstore())
.pipe(rename({ basename: path.parse(filename).name }))
.pipe(size(sizeOptions))
.pipe(gulp.dest(entry.dist)),
.pipe(gulp.dest(dist)),
);
});
@@ -234,11 +251,23 @@ gulp.task('watch', () => {
gulp.watch(paths.demo.src.sass, gulp.parallel(...tasks.css));
});
// Serve via browser sync
gulp.task('serve', () =>
browserSync.init({
server: {
baseDir: paths.demo.root,
},
notify: false,
watch: true,
ghostMode: false,
}),
);
// Build distribution
gulp.task('build', gulp.series(tasks.clean, gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite)));
// Default gulp task
gulp.task('default', gulp.series('build', 'watch'));
gulp.task('default', gulp.series('build', gulp.parallel('serve', 'watch')));
// Publish a version to CDN and demo
// --------------------------------------------
@@ -265,7 +294,7 @@ const options = {
},
},
demo: {
uploadPath: branch.current === branch.beta ? 'beta' : null,
uploadPath: branch.current === branch.beta ? '/beta' : null,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
},
@@ -320,7 +349,10 @@ gulp.task('version', done => {
const files = ['plyr.js', 'plyr.polyfilled.js', 'config/defaults.js'];
return gulp
.src(files.map(file => path.join(__dirname, `src/js/${file}`)), { base: '.' })
.src(
files.map(file => path.join(__dirname, `src/js/${file}`)),
{ base: '.' },
)
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${domain}/${version}/`))
.pipe(gulp.dest('./'));
@@ -429,6 +461,14 @@ gulp.task('demo', done => {
return gulp
.src(pages)
.pipe(replace(localPath, versionPath))
.pipe(
rename(p => {
if (options.demo.uploadPath) {
// eslint-disable-next-line no-param-reassign
p.dirname += options.demo.uploadPath;
}
}),
)
.pipe(publisher.publish(options.demo.headers))
.pipe(publish.reporter());
});
+41 -38
View File
@@ -1,10 +1,11 @@
{
"name": "plyr",
"version": "3.5.3",
"version": "3.5.7",
"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",
@@ -31,65 +32,67 @@
"scripts": {
"build": "gulp build",
"lint": "eslint src/js && npm run-script remark",
"lint:fix": "eslint --fix src/js",
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
"deploy": "yarn lint && gulp deploy"
},
"devDependencies": {
"ansi-colors": "^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",
"ansi-colors": "^4.1.1",
"aws-sdk": "^2.614.0",
"@babel/core": "^7.8.4",
"@babel/preset-env": "^7.8.4",
"babel-eslint": "^10.0.3",
"browser-sync": "^2.26.7",
"del": "^5.1.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-simple-import-sort": "^5.0.1",
"fancy-log": "^1.3.3",
"fastly-purge": "^1.0.1",
"git-branch": "^2.0.1",
"gulp": "^4.0.0",
"gulp-autoprefixer": "^6.0.0",
"gulp-awspublish": "^4.0.0",
"gulp": "^4.0.2",
"gulp-autoprefixer": "^7.0.1",
"gulp-awspublish": "^4.1.1",
"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-clean-css": "^4.2.0",
"gulp-filter": "^6.0.0",
"gulp-header": "^2.0.9",
"gulp-imagemin": "^7.1.0",
"gulp-open": "^3.0.1",
"gulp-plumber": "^1.2.1",
"gulp-postcss": "^8.0.0",
"gulp-rename": "^1.4.0",
"gulp-rename": "^2.0.0",
"gulp-replace": "^1.0.0",
"gulp-sass": "^4.0.2",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.5",
"gulp-svgstore": "^7.0.1",
"gulp-terser": "^1.1.7",
"postcss-custom-properties": "^8.0.10",
"prettier-eslint": "^8.8.2",
"gulp-terser": "^1.2.0",
"postcss-custom-properties": "^9.0.2",
"prettier-eslint": "^9.0.1",
"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.3.0",
"stylelint-order": "^2.2.1",
"stylelint-scss": "^3.5.4",
"remark-cli": "^7.0.1",
"remark-validate-links": "^9.2.0",
"rollup": "^1.31.0",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"stylelint": "^13.1.0",
"stylelint-config-prettier": "^8.0.1",
"stylelint-config-recommended": "^3.0.0",
"stylelint-config-sass-guidelines": "^7.0.0",
"stylelint-order": "^4.0.0",
"stylelint-scss": "^3.14.2",
"stylelint-selector-bem-pattern": "^2.1.0",
"through2": "^3.0.1"
},
"dependencies": {
"core-js": "^3.0.1",
"core-js": "^3.6.4",
"custom-event-polyfill": "^1.0.7",
"loadjs": "^3.6.1",
"loadjs": "^4.2.0",
"rangetouch": "^2.0.0",
"raven-js": "^3.27.0",
"url-polyfill": "^1.1.5"
"url-polyfill": "^1.1.8"
}
}
+12 -8
View File
@@ -9,25 +9,29 @@
"**/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.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
// Trim on save
"files.trimTrailingWhitespace": true
"files.trimTrailingWhitespace": true,
// Special file associations
"files.associations": {
".eslintrc": "jsonc"
},
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}
}
+54 -41
View File
@@ -1,18 +1,17 @@
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) - [![npm version](https://badge.fury.io/js/plyr.svg)](https://badge.fury.io/js/plyr)
[Checkout the demo](https://plyr.io) - [Donate](#donate) - [Slack](https://bit.ly/plyr--chat) - [![npm version](https://badge.fury.io/js/plyr.svg)](https://badge.fury.io/js/plyr)
[![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png?v=3)](https://plyr.io)
# Features
- 📼 **HTML Video & Audio, YouTube & Vimeo** - support for the major formats
- 💪 **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
- 🔧 **[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
- 📼 **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
@@ -25,7 +24,7 @@ Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vi
- 📖 **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
- 🤟 **No frameworks** - written in "vanilla" ES6 JavaScript, no jQuery required
- 💁‍♀️ **SASS** - to include in your build processes
### Demos
@@ -109,7 +108,15 @@ 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>
@@ -123,18 +130,18 @@ 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.
```html
<script src="https://cdn.plyr.io/3.5.3/plyr.js"></script>
<script src="https://cdn.plyr.io/3.5.7/plyr.js"></script>
```
...or...
```html
<script src="https://cdn.plyr.io/3.5.3/plyr.polyfilled.js"></script>
<script src="https://cdn.plyr.io/3.5.7/plyr.polyfilled.js"></script>
```
## CSS
Include the `plyr.css` stylsheet into your `<head>`
Include the `plyr.css` stylsheet into your `<head>`.
```html
<link rel="stylesheet" href="path/to/plyr.css" />
@@ -143,13 +150,13 @@ Include the `plyr.css` stylsheet into your `<head>`
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.5.3/plyr.css" />
<link rel="stylesheet" href="https://cdn.plyr.io/3.5.7/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.3/plyr.svg`.
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.5.7/plyr.svg`.
# Ads
@@ -204,7 +211,7 @@ 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 [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
@@ -212,7 +219,7 @@ _Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element
#### 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');
@@ -238,7 +245,7 @@ You have two choices here. You can either use a simple array loop to map the con
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');
@@ -269,13 +276,13 @@ 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 |
| `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. |
| `iconPrefix` | String | `plyr` | Specify the id prefix for the icons used in the default controls (e.g. "plyr-play" would be "plyr"). This is to prevent clashes if you're using your own SVG sprite but with the default controls. Most people can ignore this option. |
| `blankVideo` | String | `https://cdn.plyr.io/static/blank.mp4` | Specify a URL or path to a blank video file used to properly cancel network requests. |
| `autoplay` | Boolean | `false` | 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`&sup2; | Boolean | `false` | Autoplay the media on load. If the `autoplay` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
| `autopause`&sup1; | Boolean | `true` | Only allow one player playing at once. |
| `seekTime` | Number | `10` | The time, in seconds, to seek when a user hits fast forward or rewind. |
| `volume` | Number | `1` | A number, between 0 and 1, representing the initial volume of the player. |
@@ -295,7 +302,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `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) |
| `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] }` | `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. |
@@ -305,6 +312,11 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `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. |
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/
# API
@@ -406,6 +418,7 @@ player.fullscreen.active; // false;
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
| `pip`&sup1; | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ (on MacOS Sierra+ and iOS 10+) and Chrome 70+. |
| `ratio` | ✓ | ✓ | Gets or sets the video aspect ratio. The setter accepts a string in the same format as the `ratio` option. |
| `download` | ✓ | ✓ | Gets or sets the URL for the download button. The setter accepts a string containing a valid absolute URL. |
1. HTML5 only
@@ -616,16 +629,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 | ✓&sup1; |
| Firefox | ✓ |
| Chrome | ✓ |
| Opera | ✓ |
| Edge | ✓ |
| IE11 | ✓&sup3; |
| IE10 | ✓&sup2;&sup3; |
| Browser | Supported |
| ------------- | --------------- |
| Safari | ✓ |
| Mobile Safari | ✓&sup1; |
| Firefox | ✓ |
| Chrome | ✓ |
| Opera | ✓ |
| Edge | ✓ |
| IE11 | ✓&sup3; |
| 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)).
@@ -649,7 +662,7 @@ The arguments are:
- 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:
@@ -665,14 +678,15 @@ 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) |
# Issues
@@ -697,6 +711,7 @@ Plyr costs money to run, not only my time. I donate my time for free as I enjoy
- [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)
@@ -716,13 +731,11 @@ Plyr costs money to run, not only my time. I donate my time for free as I enjoy
- [Sparkk TV](https://www.sparkktv.com/)
- [@halfhalftravel](https://www.halfhalftravel.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)
- [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
+19 -15
View File
@@ -85,7 +85,6 @@ const captions = {
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
// Use first browser language when language is 'auto'
@@ -124,19 +123,22 @@ const captions = {
// 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',
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
// 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));
});
// Turn off native caption rendering to avoid double captions
track.mode = 'hidden';
// Add event listener for cue changes
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
});
}
// Update language first time it matches, or if the previous matching track was removed
@@ -164,7 +166,6 @@ const captions = {
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;
@@ -300,10 +301,12 @@ const captions = {
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
let track;
languages.every(language => {
track = sorted.find(track => track.language === language);
track = sorted.find(t => t.language === language);
return !track; // Break iteration if there is a match
});
// If no match is found but is required, get first
return track || (force ? sorted[0] : undefined);
},
@@ -360,6 +363,7 @@ const captions = {
// Get cues from track
if (!cues) {
const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || [])
.map(cue => cue.getCueAsHTML())
.map(getHTML);
+9 -12
View File
@@ -61,7 +61,7 @@ const defaults = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.5.3/plyr.svg',
iconUrl: 'https://cdn.plyr.io/3.5.7/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@@ -69,7 +69,10 @@ const defaults = {
// 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
@@ -82,7 +85,8 @@ const defaults = {
// Speed default and options to display
speed: {
selected: 1,
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
// 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
@@ -128,6 +132,7 @@ const defaults = {
// 'fast-forward',
'progress',
'current-time',
// 'duration',
'mute',
'volume',
'captions',
@@ -163,6 +168,7 @@ const defaults = {
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
pip: 'PIP',
menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
@@ -195,8 +201,7 @@ const defaults = {
},
youtube: {
sdk: 'https://www.youtube.com/iframe_api',
api:
'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',
},
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
@@ -320,9 +325,6 @@ const defaults = {
progress: '.plyr__progress',
captions: '.plyr__captions',
caption: '.plyr__caption',
menu: {
quality: '.js-plyr__menu__list--quality',
},
},
// Class hooks added to the player in different states
@@ -396,11 +398,6 @@ const defaults = {
},
},
// API keys
keys: {
google: null,
},
// Advertisements plugin
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: {
+353 -315
View File
@@ -4,11 +4,12 @@
// ==========================================================================
import RangeTouch from 'rangetouch';
import captions from './captions';
import html5 from './html5';
import support from './support';
import { repaint, transitionEndEvent } from './utils/animation';
import { dedupe } from './utils/arrays';
import { dedupe, fillRange } from './utils/arrays';
import browser from './utils/browser';
import {
createElement,
@@ -27,7 +28,7 @@ import {
import { off, on } from './utils/events';
import i18n from './utils/i18n';
import is from './utils/is';
import loadSprite from './utils/loadSprite';
import loadSprite from './utils/load-sprite';
import { extend } from './utils/objects';
import { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';
import { formatTime, getHours } from './utils/time';
@@ -105,7 +106,6 @@ const controls = {
const namespace = 'http://www.w3.org/2000/svg';
const iconUrl = controls.getIconUrl.call(this);
const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;
// Create <svg>
const icon = document.createElementNS(namespace, 'svg');
setAttributes(
@@ -139,10 +139,7 @@ const controls = {
// Create hidden text label
createLabel(key, attr = {}) {
const text = i18n.get(key, this.config);
const attributes = Object.assign({}, attr, {
class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' '),
});
const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') };
return createElement('span', attributes, text);
},
@@ -172,7 +169,7 @@ const controls = {
// Create a <button>
createButton(buttonType, attr) {
const attributes = Object.assign({}, attr);
const attributes = extend({}, attr);
let type = toCamelCase(buttonType);
const props = {
@@ -198,8 +195,10 @@ const controls = {
// Set class name
if (Object.keys(attributes).includes('class')) {
if (!attributes.class.includes(this.config.classNames.control)) {
attributes.class += ` ${this.config.classNames.control}`;
if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {
extend(attributes, {
class: `${attributes.class} ${this.config.classNames.control}`,
});
}
} else {
attributes.class = this.config.classNames.control;
@@ -377,13 +376,13 @@ const controls = {
},
// Create time display
createTime(type) {
const attributes = getAttributesFromSelector(this.config.selectors.display[type]);
createTime(type, attrs) {
const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);
const container = createElement(
'div',
extend(attributes, {
class: `${this.config.classNames.display.time} ${attributes.class ? attributes.class : ''}`.trim(),
class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),
'aria-label': i18n.get(type, this.config),
}),
'00:00',
@@ -400,7 +399,8 @@ const controls = {
// https://bugzilla.mozilla.org/show_bug.cgi?id=1220143
bindMenuItemShortcuts(menuItem, type) {
// Navigate through menus via arrow keys and space
on(
on.call(
this,
menuItem,
'keydown keyup',
event => {
@@ -450,7 +450,7 @@ const controls = {
// Enter will fire a `click` event but we still need to manage focus
// So we bind to keyup which fires after and set focus here
on(menuItem, 'keyup', event => {
on.call(this, menuItem, 'keyup', event => {
if (event.which !== 13) {
return;
}
@@ -491,15 +491,15 @@ const controls = {
get() {
return menuItem.getAttribute('aria-checked') === 'true';
},
set(checked) {
set(check) {
// Ensure exclusivity
if (checked) {
if (check) {
Array.from(menuItem.parentNode.children)
.filter(node => matches(node, '[role="menuitemradio"]'))
.forEach(node => node.setAttribute('aria-checked', 'false'));
}
menuItem.setAttribute('aria-checked', checked ? 'true' : 'false');
menuItem.setAttribute('aria-checked', check ? 'true' : 'false');
},
});
@@ -607,17 +607,17 @@ const controls = {
let value = 0;
const setProgress = (target, input) => {
const value = is.number(input) ? input : 0;
const val = is.number(input) ? input : 0;
const progress = is.element(target) ? target : this.elements.display.buffer;
// Update value and label
if (is.element(progress)) {
progress.value = value;
progress.value = val;
// Update text label inside
const label = progress.getElementsByTagName('span')[0];
if (is.element(label)) {
label.childNodes[0].nodeValue = value;
label.childNodes[0].nodeValue = val;
}
}
};
@@ -699,14 +699,8 @@ const controls = {
return;
}
// Calculate percentage
let percent = 0;
const clientRect = this.elements.progress.getBoundingClientRect();
const visible = `${this.config.classNames.tooltip}--visible`;
const toggle = toggle => {
toggleClass(this.elements.display.seekTooltip, visible, toggle);
};
const toggle = show => toggleClass(this.elements.display.seekTooltip, visible, show);
// Hide on touch
if (this.touch) {
@@ -715,6 +709,9 @@ const controls = {
}
// Determine percentage, if already visible
let percent = 0;
const clientRect = this.elements.progress.getBoundingClientRect();
if (is.event(event)) {
percent = (100 / clientRect.width) * (event.pageX - clientRect.left);
} else if (hasClass(this.elements.display.seekTooltip, visible)) {
@@ -1047,7 +1044,7 @@ const controls = {
},
// Set a list of available captions languages
setSpeedMenu(options) {
setSpeedMenu() {
// Menu required
if (!is.element(this.elements.settings.panels.speed)) {
return;
@@ -1056,16 +1053,14 @@ const controls = {
const type = 'speed';
const list = this.elements.settings.panels.speed.querySelector('[role="menu"]');
// Set the speed options
if (is.array(options)) {
this.options.speed = options;
} else if (this.isHTML5 || this.isVimeo) {
this.options.speed = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
// Determine options to display
// Vimeo and YouTube limit to 0.5x-2x
if (this.isVimeo || this.isYouTube) {
this.options.speed = fillRange(0.5, 2, 0.25).filter(s => this.config.speed.options.includes(s));
} else {
this.options.speed = this.config.speed.options;
}
// Set options if passed and filter based on config
this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed));
// Toggle the pane and tab
const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;
controls.toggleMenuButton.call(this, type, toggle);
@@ -1111,7 +1106,7 @@ const controls = {
let target = pane;
if (!is.element(target)) {
target = Object.values(this.elements.settings.panels).find(pane => !pane.hidden);
target = Object.values(this.elements.settings.panels).find(p => !p.hidden);
}
const firstItem = target.querySelector('[role^="menuitem"]');
@@ -1138,7 +1133,10 @@ const controls = {
} else if (is.keyboardEvent(input) && input.which === 27) {
show = false;
} else if (is.event(input)) {
const isMenuItem = popup.contains(input.target);
// If Plyr is in a shadowDOM, the event target is set to the component, instead of the
// Element in the shadowDOM. The path, if available, is complete.
const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;
const isMenuItem = popup.contains(target);
// If the click was inside the menu or if the click
// wasn't the button or menu item and we're trying to
@@ -1191,7 +1189,7 @@ const controls = {
// Show a panel in the menu
showMenuPanel(type = '', tabFocus = false) {
const target = document.getElementById(`plyr-settings-${this.id}-${type}`);
const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);
// Nothing to show, bail
if (!is.element(target)) {
@@ -1244,8 +1242,8 @@ const controls = {
controls.focusFirstMenuItem.call(this, target, tabFocus);
},
// Set the download link
setDownloadLink() {
// Set the download URL
setDownloadUrl() {
const button = this.elements.buttons.download;
// Bail if no button
@@ -1253,324 +1251,364 @@ const controls = {
return;
}
// Set download link
// Set attribute
button.setAttribute('href', this.download);
},
// Build the default HTML
// TODO: Set order based on order in the config.controls array?
create(data) {
const {
bindMenuItemShortcuts,
createButton,
createProgress,
createRange,
createTime,
setQualityMenu,
setSpeedMenu,
showMenuPanel,
} = controls;
this.elements.controls = null;
// Larger overlaid play button
if (this.config.controls.includes('play-large')) {
this.elements.container.appendChild(createButton.call(this, 'play-large'));
}
// Create the container
const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));
this.elements.controls = container;
// Restart button
if (this.config.controls.includes('restart')) {
container.appendChild(controls.createButton.call(this, 'restart'));
}
// Default item attributes
const defaultAttributes = { class: 'plyr__controls__item' };
// Rewind button
if (this.config.controls.includes('rewind')) {
container.appendChild(controls.createButton.call(this, 'rewind'));
}
// Loop through controls in order
dedupe(this.config.controls).forEach(control => {
// Restart button
if (control === 'restart') {
container.appendChild(createButton.call(this, 'restart', defaultAttributes));
}
// Play/Pause button
if (this.config.controls.includes('play')) {
container.appendChild(controls.createButton.call(this, 'play'));
}
// Rewind button
if (control === 'rewind') {
container.appendChild(createButton.call(this, 'rewind', defaultAttributes));
}
// Fast forward button
if (this.config.controls.includes('fast-forward')) {
container.appendChild(controls.createButton.call(this, 'fast-forward'));
}
// Play/Pause button
if (control === 'play') {
container.appendChild(createButton.call(this, 'play', defaultAttributes));
}
// Progress
if (this.config.controls.includes('progress')) {
const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
// Fast forward button
if (control === 'fast-forward') {
container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));
}
// Seek range slider
progress.appendChild(
controls.createRange.call(this, 'seek', {
id: `plyr-seek-${data.id}`,
}),
);
// Progress
if (control === 'progress') {
const progressContainer = createElement('div', {
class: `${defaultAttributes.class} plyr__progress__container`,
});
// Buffer progress
progress.appendChild(controls.createProgress.call(this, 'buffer'));
const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
// TODO: Add loop display indicator
// Seek tooltip
if (this.config.tooltips.seek) {
const tooltip = createElement(
'span',
{
class: this.config.classNames.tooltip,
},
'00:00',
// Seek range slider
progress.appendChild(
createRange.call(this, 'seek', {
id: `plyr-seek-${data.id}`,
}),
);
progress.appendChild(tooltip);
this.elements.display.seekTooltip = tooltip;
// Buffer progress
progress.appendChild(createProgress.call(this, 'buffer'));
// TODO: Add loop display indicator
// Seek tooltip
if (this.config.tooltips.seek) {
const tooltip = createElement(
'span',
{
class: this.config.classNames.tooltip,
},
'00:00',
);
progress.appendChild(tooltip);
this.elements.display.seekTooltip = tooltip;
}
this.elements.progress = progress;
progressContainer.appendChild(this.elements.progress);
container.appendChild(progressContainer);
}
this.elements.progress = progress;
container.appendChild(this.elements.progress);
}
// Media current time display
if (this.config.controls.includes('current-time')) {
container.appendChild(controls.createTime.call(this, 'currentTime'));
}
// Media duration display
if (this.config.controls.includes('duration')) {
container.appendChild(controls.createTime.call(this, 'duration'));
}
// Volume controls
if (this.config.controls.includes('mute') || this.config.controls.includes('volume')) {
const volume = createElement('div', {
class: 'plyr__volume',
});
// Toggle mute button
if (this.config.controls.includes('mute')) {
volume.appendChild(controls.createButton.call(this, 'mute'));
// Media current time display
if (control === 'current-time') {
container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));
}
// Volume range control
if (this.config.controls.includes('volume')) {
// Set the attributes
const attributes = {
max: 1,
step: 0.05,
value: this.config.volume,
};
// Media duration display
if (control === 'duration') {
container.appendChild(createTime.call(this, 'duration', defaultAttributes));
}
// Create the volume range slider
volume.appendChild(
controls.createRange.call(
this,
'volume',
extend(attributes, {
id: `plyr-volume-${data.id}`,
// Volume controls
if (control === 'mute' || control === 'volume') {
let { volume } = this.elements;
// Create the volume container if needed
if (!is.element(volume) || !container.contains(volume)) {
volume = createElement(
'div',
extend({}, defaultAttributes, {
class: `${defaultAttributes.class} plyr__volume`.trim(),
}),
),
);
);
this.elements.volume = volume;
this.elements.volume = volume;
container.appendChild(volume);
}
// Toggle mute button
if (control === 'mute') {
volume.appendChild(createButton.call(this, 'mute'));
}
// Volume range control
// Ignored on iOS as it's handled globally
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
if (control === 'volume' && !browser.isIos) {
// Set the attributes
const attributes = {
max: 1,
step: 0.05,
value: this.config.volume,
};
// Create the volume range slider
volume.appendChild(
createRange.call(
this,
'volume',
extend(attributes, {
id: `plyr-volume-${data.id}`,
}),
),
);
}
}
container.appendChild(volume);
}
// Toggle captions button
if (control === 'captions') {
container.appendChild(createButton.call(this, 'captions', defaultAttributes));
}
// Toggle captions button
if (this.config.controls.includes('captions')) {
container.appendChild(controls.createButton.call(this, 'captions'));
}
// Settings button / menu
if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
const control = createElement('div', {
class: 'plyr__menu',
hidden: '',
});
control.appendChild(
controls.createButton.call(this, 'settings', {
'aria-haspopup': true,
'aria-controls': `plyr-settings-${data.id}`,
'aria-expanded': false,
}),
);
const popup = createElement('div', {
class: 'plyr__menu__container',
id: `plyr-settings-${data.id}`,
hidden: '',
});
const inner = createElement('div');
const home = createElement('div', {
id: `plyr-settings-${data.id}-home`,
});
// Create the menu
const menu = createElement('div', {
role: 'menu',
});
home.appendChild(menu);
inner.appendChild(home);
this.elements.settings.panels.home = home;
// Build the menu items
this.config.settings.forEach(type => {
// TODO: bundle this with the createMenuItem helper and bindings
const menuItem = createElement(
'button',
extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
type: 'button',
class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
role: 'menuitem',
'aria-haspopup': true,
// Settings button / menu
if (control === 'settings' && !is.empty(this.config.settings)) {
const wrapper = createElement(
'div',
extend({}, defaultAttributes, {
class: `${defaultAttributes.class} plyr__menu`.trim(),
hidden: '',
}),
);
// Bind menu shortcuts for keyboard users
controls.bindMenuItemShortcuts.call(this, menuItem, type);
// Show menu on click
on(menuItem, 'click', () => {
controls.showMenuPanel.call(this, type, false);
});
const flex = createElement('span', null, i18n.get(type, this.config));
const value = createElement('span', {
class: this.config.classNames.menu.value,
});
// Speed contains HTML entities
value.innerHTML = data[type];
flex.appendChild(value);
menuItem.appendChild(flex);
menu.appendChild(menuItem);
// Build the panes
const pane = createElement('div', {
id: `plyr-settings-${data.id}-${type}`,
hidden: '',
});
// Back button
const backButton = createElement('button', {
type: 'button',
class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,
});
// Visible label
backButton.appendChild(
createElement(
'span',
{
'aria-hidden': true,
},
i18n.get(type, this.config),
),
);
// Screen reader label
backButton.appendChild(
createElement(
'span',
{
class: this.config.classNames.hidden,
},
i18n.get('menuBack', this.config),
),
);
// Go back via keyboard
on(
pane,
'keydown',
event => {
// We only care about <-
if (event.which !== 37) {
return;
}
// Prevent seek
event.preventDefault();
event.stopPropagation();
// Show the respective menu
controls.showMenuPanel.call(this, 'home', true);
},
false,
);
// Go back via button click
on(backButton, 'click', () => {
controls.showMenuPanel.call(this, 'home', false);
});
// Add to pane
pane.appendChild(backButton);
// Menu
pane.appendChild(
createElement('div', {
role: 'menu',
wrapper.appendChild(
createButton.call(this, 'settings', {
'aria-haspopup': true,
'aria-controls': `plyr-settings-${data.id}`,
'aria-expanded': false,
}),
);
inner.appendChild(pane);
this.elements.settings.buttons[type] = menuItem;
this.elements.settings.panels[type] = pane;
});
popup.appendChild(inner);
control.appendChild(popup);
container.appendChild(control);
this.elements.settings.popup = popup;
this.elements.settings.menu = control;
}
// Picture in picture button
if (this.config.controls.includes('pip') && support.pip) {
container.appendChild(controls.createButton.call(this, 'pip'));
}
// Airplay button
if (this.config.controls.includes('airplay') && support.airplay) {
container.appendChild(controls.createButton.call(this, 'airplay'));
}
// Download button
if (this.config.controls.includes('download')) {
const attributes = {
element: 'a',
href: this.download,
target: '_blank',
};
const { download } = this.config.urls;
if (!is.url(download) && this.isEmbed) {
extend(attributes, {
icon: `logo-${this.provider}`,
label: this.provider,
const popup = createElement('div', {
class: 'plyr__menu__container',
id: `plyr-settings-${data.id}`,
hidden: '',
});
const inner = createElement('div');
const home = createElement('div', {
id: `plyr-settings-${data.id}-home`,
});
// Create the menu
const menu = createElement('div', {
role: 'menu',
});
home.appendChild(menu);
inner.appendChild(home);
this.elements.settings.panels.home = home;
// Build the menu items
this.config.settings.forEach(type => {
// TODO: bundle this with the createMenuItem helper and bindings
const menuItem = createElement(
'button',
extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
type: 'button',
class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
role: 'menuitem',
'aria-haspopup': true,
hidden: '',
}),
);
// Bind menu shortcuts for keyboard users
bindMenuItemShortcuts.call(this, menuItem, type);
// Show menu on click
on.call(this, menuItem, 'click', () => {
showMenuPanel.call(this, type, false);
});
const flex = createElement('span', null, i18n.get(type, this.config));
const value = createElement('span', {
class: this.config.classNames.menu.value,
});
// Speed contains HTML entities
value.innerHTML = data[type];
flex.appendChild(value);
menuItem.appendChild(flex);
menu.appendChild(menuItem);
// Build the panes
const pane = createElement('div', {
id: `plyr-settings-${data.id}-${type}`,
hidden: '',
});
// Back button
const backButton = createElement('button', {
type: 'button',
class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,
});
// Visible label
backButton.appendChild(
createElement(
'span',
{
'aria-hidden': true,
},
i18n.get(type, this.config),
),
);
// Screen reader label
backButton.appendChild(
createElement(
'span',
{
class: this.config.classNames.hidden,
},
i18n.get('menuBack', this.config),
),
);
// Go back via keyboard
on.call(
this,
pane,
'keydown',
event => {
// We only care about <-
if (event.which !== 37) {
return;
}
// Prevent seek
event.preventDefault();
event.stopPropagation();
// Show the respective menu
showMenuPanel.call(this, 'home', true);
},
false,
);
// Go back via button click
on.call(this, backButton, 'click', () => {
showMenuPanel.call(this, 'home', false);
});
// Add to pane
pane.appendChild(backButton);
// Menu
pane.appendChild(
createElement('div', {
role: 'menu',
}),
);
inner.appendChild(pane);
this.elements.settings.buttons[type] = menuItem;
this.elements.settings.panels[type] = pane;
});
popup.appendChild(inner);
wrapper.appendChild(popup);
container.appendChild(wrapper);
this.elements.settings.popup = popup;
this.elements.settings.menu = wrapper;
}
container.appendChild(controls.createButton.call(this, 'download', attributes));
}
// Picture in picture button
if (control === 'pip' && support.pip) {
container.appendChild(createButton.call(this, 'pip', defaultAttributes));
}
// Toggle fullscreen button
if (this.config.controls.includes('fullscreen')) {
container.appendChild(controls.createButton.call(this, 'fullscreen'));
}
// Airplay button
if (control === 'airplay' && support.airplay) {
container.appendChild(createButton.call(this, 'airplay', defaultAttributes));
}
// Larger overlaid play button
if (this.config.controls.includes('play-large')) {
this.elements.container.appendChild(controls.createButton.call(this, 'play-large'));
}
// Download button
if (control === 'download') {
const attributes = extend({}, defaultAttributes, {
element: 'a',
href: this.download,
target: '_blank',
});
this.elements.controls = container;
// Set download attribute for HTML5 only
if (this.isHTML5) {
attributes.download = '';
}
const { download } = this.config.urls;
if (!is.url(download) && this.isEmbed) {
extend(attributes, {
icon: `logo-${this.provider}`,
label: this.provider,
});
}
container.appendChild(createButton.call(this, 'download', attributes));
}
// Toggle fullscreen button
if (control === 'fullscreen') {
container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));
}
});
// Set available quality levels
if (this.isHTML5) {
controls.setQualityMenu.call(this, html5.getQualityOptions.call(this));
setQualityMenu.call(this, html5.getQualityOptions.call(this));
}
controls.setSpeedMenu.call(this);
setSpeedMenu.call(this);
return container;
},
+99 -78
View File
@@ -4,84 +4,11 @@
// 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 { getElements, hasClass, toggleClass } from './utils/elements';
import { on, triggerEvent } from './utils/events';
import is from './utils/is';
function onChange() {
if (!this.enabled) {
return;
}
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (is.element(button)) {
button.pressed = this.active;
}
// Trigger an event
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
// Trap focus in container
if (!browser.isIos) {
trapFocus.call(this.player, this.target, this.active);
}
}
function toggleFallback(toggle = false) {
// Store or restore scroll position
if (toggle) {
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
}
// Toggle scroll
document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Force full viewport on iPhone X+
if (browser.isIos) {
let viewport = document.head.querySelector('meta[name="viewport"]');
const property = 'viewport-fit=cover';
// Inject the viewport meta if required
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
}
// Check if the property already exists
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) {
viewport.content += `,${property}`;
}
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
.filter(part => part.trim() !== property)
.join(',');
}
// Force a repaint as sometimes Safari doesn't want to fill the screen
setTimeout(() => repaint(this.target), 100);
}
// Toggle button and fire events
onChange.call(this);
}
class Fullscreen {
constructor(player) {
// Keep reference to parent
@@ -105,7 +32,7 @@ class Fullscreen {
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => {
// TODO: Filter for target??
onChange.call(this);
this.onChange();
},
);
@@ -119,6 +46,9 @@ class Fullscreen {
this.toggle();
});
// Tap focus when in fullscreen
on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));
// Update the UI
this.update();
}
@@ -198,6 +128,97 @@ class 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;
}
// Trigger an event
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
}
toggleFallback(toggle = false) {
// Store or restore scroll position
if (toggle) {
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
}
// Toggle scroll
document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Force full viewport on iPhone X+
if (browser.isIos) {
let viewport = document.head.querySelector('meta[name="viewport"]');
const property = 'viewport-fit=cover';
// Inject the viewport meta if required
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
}
// Check if the property already exists
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) {
viewport.content += `,${property}`;
}
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
.filter(part => part.trim() !== property)
.join(',');
}
}
// Toggle button and fire events
this.onChange();
}
// Trap focus inside container
trapFocus(event) {
// Bail if iOS, not active, not the tab key
if (browser.isIos || !this.active || event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element
const focused = document.activeElement;
const focusable = getElements.call(
this.player,
'a[href], button:not(:disabled), input:not(:disabled), [tabindex]',
);
const [first] = focusable;
const last = focusable[focusable.length - 1];
if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used
first.focus();
event.preventDefault();
} else if (focused === first && event.shiftKey) {
// Move focus to last element that can be tabbed if Shift is used
last.focus();
event.preventDefault();
}
}
// Update UI
update() {
if (this.enabled) {
@@ -230,9 +251,9 @@ class Fullscreen {
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitEnterFullscreen();
} else if (!Fullscreen.native || this.forceFallback) {
toggleFallback.call(this, true);
this.toggleFallback(true);
} else if (!this.prefix) {
this.target.requestFullscreen();
this.target.requestFullscreen({ navigationUI: 'hide' });
} else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`]();
}
@@ -249,7 +270,7 @@ class Fullscreen {
this.target.webkitExitFullscreen();
this.player.play();
} else if (!Fullscreen.native || this.forceFallback) {
toggleFallback.call(this, false);
this.toggleFallback(false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!is.empty(this.prefix)) {
+44 -27
View File
@@ -30,6 +30,11 @@ const html5 = {
// 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)
@@ -44,51 +49,63 @@ const html5 = {
const player = this;
// Set aspect ratio if set
setAspectRatio.call(player);
// 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(source => source.getAttribute('src') === player.source);
const source = sources.find(s => s.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) {
if (player.quality === input) {
return;
}
// Get current state
const { currentTime, paused, preload, readyState } = player.media;
// If we're using an an external handler...
if (player.config.quality.forced && is.function(player.config.quality.onChange)) {
player.config.quality.onChange(input);
} else {
// Get sources
const sources = html5.getSources.call(player);
// Get first match for requested size
const source = sources.find(s => Number(s.getAttribute('size')) === input);
// Set new source
player.media.src = source.getAttribute('src');
// No matching source found
if (!source) {
return;
}
// 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;
// Get current state
const { currentTime, paused, preload, readyState, playbackRate } = player.media;
// Resume playing
if (!paused) {
player.play();
}
});
// Set new source
player.media.src = source.getAttribute('src');
// Load new source
player.media.load();
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
if (preload !== 'none' || readyState) {
// Restore time
player.once('loadedmetadata', () => {
player.speed = playbackRate;
player.currentTime = currentTime;
// Resume playing
if (!paused) {
player.play();
}
});
// Load new source
player.media.load();
}
}
// Trigger change event
+36 -43
View File
@@ -6,7 +6,7 @@ import controls from './controls';
import ui from './ui';
import { repaint } from './utils/animation';
import browser from './utils/browser';
import { getElement, getElements, matches, toggleClass, toggleHidden } from './utils/elements';
import { getElement, getElements, matches, toggleClass } from './utils/elements';
import { off, on, once, toggleListener, triggerEvent } from './utils/events';
import is from './utils/is';
import { getAspectRatio, setAspectRatio } from './utils/style';
@@ -147,7 +147,7 @@ class Listeners {
player.loop = !player.loop;
break;
/* case 73:
/* case 73:
this.setLoop('start');
break;
@@ -275,17 +275,16 @@ class Listeners {
elements.container,
'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',
event => {
const { controls } = elements;
const { controls: controlsElement } = elements;
// Remove button states for fullscreen
if (controls && event.type === 'enterfullscreen') {
controls.pressed = false;
controls.hover = false;
if (controlsElement && event.type === 'enterfullscreen') {
controlsElement.pressed = false;
controlsElement.hover = false;
}
// Show, then hide after a timeout unless another control event occurs
const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
let delay = 0;
if (show) {
@@ -302,14 +301,6 @@ class Listeners {
},
);
// Force edge to repaint on exit fullscreen
// TODO: Fix weird bug where Edge doesn't re-draw when exiting fullscreen
/* if (browser.isEdge) {
on.call(player, elements.container, 'exitfullscreen', () => {
setTimeout(() => repaint(elements.container), 100);
});
} */
// Set a gutter for Vimeo
const setGutter = (ratio, padding, toggle) => {
if (!player.isVimeo) {
@@ -318,7 +309,7 @@ class Listeners {
const target = player.elements.wrapper.firstChild;
const [, y] = ratio;
const [videoX, videoY] = getAspectRatio.call(this);
const [videoX, videoY] = getAspectRatio.call(player);
target.style.maxWidth = toggle ? `${(y / videoY) * videoX}px` : null;
target.style.margin = toggle ? '0 auto' : null;
@@ -338,20 +329,24 @@ class Listeners {
};
const resized = () => {
window.clearTimeout(timers.resized);
timers.resized = window.setTimeout(setPlayerSize, 50);
clearTimeout(timers.resized);
timers.resized = setTimeout(setPlayerSize, 50);
};
on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {
const { target, usingNative } = player.fullscreen;
// Ignore for iOS native
if (!player.isEmbed || target !== elements.container) {
// Ignore events not from target
if (target !== elements.container) {
return;
}
// If it's not an embed and no ratio specified
if (!player.isEmbed && is.empty(player.config.ratio)) {
return;
}
const isEnter = event.type === 'enterfullscreen';
// Set the player size when entering fullscreen to viewport size
const { padding, ratio } = setPlayerSize(isEnter);
@@ -382,19 +377,15 @@ class Listeners {
controls.durationUpdate.call(player, event),
);
// Check for audio tracks on load
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
on.call(player, player.media, 'canplay loadeddata', () => {
toggleHidden(elements.volume, !player.hasAudio);
toggleHidden(elements.buttons.mute, !player.hasAudio);
});
// Handle the media finishing
on.call(player, player.media, 'ended', () => {
// Show poster on end
if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {
// Restart
player.restart();
// Call pause otherwise IE11 will start playing the video again
player.pause();
}
});
@@ -486,7 +477,7 @@ class Listeners {
// Update download link when ready and if quality changes
on.call(player, player.media, 'ready qualitychange', () => {
controls.setDownloadLink.call(player);
controls.setDownloadUrl.call(player);
});
// Proxy events to container
@@ -518,7 +509,7 @@ class Listeners {
}
// Only call default handler if not prevented in custom handler
if (returned && is.function(defaultHandler)) {
if (returned !== false && is.function(defaultHandler)) {
defaultHandler.call(player, event);
}
}
@@ -542,7 +533,6 @@ class Listeners {
controls() {
const { player } = this;
const { elements } = player;
// IE doesn't support input event, so we fallback to change
const inputEvent = browser.isIE ? 'change' : 'input';
@@ -609,12 +599,19 @@ class Listeners {
this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');
// Settings menu - click toggle
this.bind(elements.buttons.settings, 'click', event => {
// Prevent the document click listener closing the menu
event.stopPropagation();
this.bind(
elements.buttons.settings,
'click',
event => {
// Prevent the document click listener closing the menu
event.stopPropagation();
event.preventDefault();
controls.toggleMenu.call(player, event);
});
controls.toggleMenu.call(player, event);
},
null,
false
); // Can't be passive as we're preventing default
// Settings menu - keyboard toggle
// We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus
@@ -669,7 +666,7 @@ class Listeners {
const code = event.keyCode ? event.keyCode : event.which;
const attribute = 'play-on-seeked';
if (is.keyboardEvent(event) && (code !== 39 && code !== 37)) {
if (is.keyboardEvent(event) && code !== 39 && code !== 37) {
return;
}
@@ -678,7 +675,6 @@ class Listeners {
// Was playing before?
const play = seek.hasAttribute(attribute);
// Done seeking
const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
@@ -706,7 +702,6 @@ class Listeners {
inputEvent,
event => {
const seek = event.currentTarget;
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
let seekTo = seek.getAttribute('seek-value');
@@ -737,7 +732,7 @@ class Listeners {
});
// Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering
this.bind(elements.progress, 'mouseleave click', () => {
this.bind(elements.progress, 'mouseleave touchend click', () => {
const { previewThumbnails } = player;
if (previewThumbnails && previewThumbnails.loaded) {
@@ -806,7 +801,7 @@ class Listeners {
// Show controls when they receive focus (e.g., when using keyboard tab key)
this.bind(elements.controls, 'focusin', () => {
const { config, elements, timers } = player;
const { config, timers } = player;
// Skip transition to prevent focus from scrolling the parent element
toggleClass(elements.controls, config.classNames.noTransition, true);
@@ -837,10 +832,8 @@ class Listeners {
// Detect "natural" scroll - suppored on OS X Safari only
// Other browsers on OS X will be inverted until support improves
const inverted = event.webkitDirectionInvertedFromDevice;
// Get delta from event. Invert if `inverted` is true
const [x, y] = [event.deltaX, -event.deltaY].map(value => (inverted ? -value : value));
// Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)
const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);
+6 -4
View File
@@ -39,11 +39,13 @@ const media = {
wrap(this.media, this.elements.wrapper);
// Faux poster container
this.elements.poster = createElement('div', {
class: this.config.classNames.poster,
});
if (this.isEmbed) {
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) {
+54 -41
View File
@@ -10,10 +10,24 @@ import { createElement } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import i18n from '../utils/i18n';
import is from '../utils/is';
import loadScript from '../utils/loadScript';
import loadScript from '../utils/load-script';
import { formatTime } from '../utils/time';
import { buildUrlParams } from '../utils/urls';
const destroy = instance => {
// Destroy our adsManager
if (instance.manager) {
instance.manager.destroy();
}
// Destroy our adsManager
if (instance.elements.displayContainer) {
instance.elements.displayContainer.destroy();
}
instance.elements.container.remove();
};
class Ads {
/**
* Ads constructor.
@@ -63,20 +77,22 @@ class Ads {
* Load the IMA SDK
*/
load() {
if (this.enabled) {
// Check if the Google IMA3 SDK is loaded or load it ourselves
if (!is.object(window.google) || !is.object(window.google.ima)) {
loadScript(this.player.config.urls.googleIMA.sdk)
.then(() => {
this.ready();
})
.catch(() => {
// Script failed to load or is blocked
this.trigger('error', new Error('Google IMA SDK failed to load'));
});
} else {
this.ready();
}
if (!this.enabled) {
return;
}
// Check if the Google IMA3 SDK is loaded or load it ourselves
if (!is.object(window.google) || !is.object(window.google.ima)) {
loadScript(this.player.config.urls.googleIMA.sdk)
.then(() => {
this.ready();
})
.catch(() => {
// Script failed to load or is blocked
this.trigger('error', new Error('Google IMA SDK failed to load'));
});
} else {
this.ready();
}
}
@@ -84,6 +100,11 @@ class Ads {
* Get the ads instance ready
*/
ready() {
// Double check we're enabled
if (!this.enabled) {
destroy(this);
}
// Start ticking our safety timer. If the whole advertisement
// thing doesn't resolve within our set time; we bail
this.startSafetyTimer(12000, 'ready()');
@@ -115,7 +136,7 @@ class Ads {
cb: Date.now(),
AV_WIDTH: 640,
AV_HEIGHT: 480,
AV_CDIM2: this.publisherId,
AV_CDIM2: config.publisherId,
};
const base = 'https://go.aniview.com/api/adserver6/vast/';
@@ -240,16 +261,13 @@ class Ads {
// Get the cue points for any mid-rolls by filtering out the pre- and post-roll
this.cuePoints = this.manager.getCuePoints();
// Set volume to match player
this.manager.setVolume(this.player.volume);
// Add listeners to the required events
// Advertisement error events
this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));
// Advertisement regular events
Object.keys(google.ima.AdEvent.Type).forEach(type => {
this.manager.addEventListener(google.ima.AdEvent.Type[type], event => this.onAdEvent(event));
this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e));
});
// Resolve our adsManager
@@ -285,7 +303,6 @@ class Ads {
*/
onAdEvent(event) {
const { container } = this.player.elements;
// Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)
// don't have ad object associated
const ad = event.getAd();
@@ -293,19 +310,18 @@ class Ads {
// Proxy event
const dispatchEvent = type => {
const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
triggerEvent.call(this.player, this.player.media, event);
triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);
};
// Bubble the event
dispatchEvent(event.type);
switch (event.type) {
case google.ima.AdEvent.Type.LOADED:
// This is the first event sent for an ad - it is possible to determine whether the
// ad is a video ad or an overlay
this.trigger('loaded');
// Bubble event
dispatchEvent(event.type);
// Start countdown
this.pollCountdown(true);
@@ -317,15 +333,19 @@ class Ads {
// console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());
// console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());
break;
case google.ima.AdEvent.Type.STARTED:
// Set volume to match player
this.manager.setVolume(this.player.volume);
break;
case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
// All ads for the current videos are done. We can now request new advertisements
// in case the video is re-played
// Fire event
dispatchEvent(event.type);
// TODO: Example for what happens when a next video in a playlist would be loaded.
// So here we load a new video when all ads are done.
// Then we load new ads within a new adsManager. When the video
@@ -350,6 +370,7 @@ class Ads {
// playing when the IMA SDK is ready or has failed
this.loadAds();
break;
case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
@@ -357,8 +378,6 @@ class Ads {
// for example display a pause button and remaining time. Fired when content should
// be paused. This usually happens right before an ad is about to cover the content
dispatchEvent(event.type);
this.pauseContent();
break;
@@ -369,26 +388,17 @@ class Ads {
// Fired when content should be resumed. This usually happens when an ad finishes
// or collapses
dispatchEvent(event.type);
this.pollCountdown();
this.resumeContent();
break;
case google.ima.AdEvent.Type.STARTED:
case google.ima.AdEvent.Type.MIDPOINT:
case google.ima.AdEvent.Type.COMPLETE:
case google.ima.AdEvent.Type.IMPRESSION:
case google.ima.AdEvent.Type.CLICK:
dispatchEvent(event.type);
break;
case google.ima.AdEvent.Type.LOG:
if (adData.adError) {
this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);
}
break;
default:
@@ -463,6 +473,9 @@ class Ads {
// Play the requested advertisement whenever the adsManager is ready
this.managerPromise
.then(() => {
// Set volume to match player
this.manager.setVolume(this.player.volume);
// Initialize the container. Must be done via a user action on mobile devices
this.elements.displayContainer.initialize();
@@ -63,6 +63,20 @@ const parseVtt = vttDataString => {
* - 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.
@@ -100,6 +114,10 @@ class PreviewThumbnails {
}
this.getThumbnails().then(() => {
if (!this.enabled) {
return;
}
// Render DOM elements
this.render();
@@ -121,7 +139,6 @@ class PreviewThumbnails {
// 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));
@@ -149,9 +166,11 @@ class PreviewThumbnails {
// 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('/') &&
if (
!thumbnail.frames[0].text.startsWith('/') &&
!thumbnail.frames[0].text.startsWith('http://') &&
!thumbnail.frames[0].text.startsWith('https://')) {
!thumbnail.frames[0].text.startsWith('https://')
) {
thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
}
@@ -220,8 +239,8 @@ class PreviewThumbnails {
}
startScrubbing(event) {
// Only act on left mouse button (0), or touch device (event.button is false)
if (event.button === false || event.button === 0) {
// 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
@@ -297,7 +316,9 @@ class PreviewThumbnails {
this.elements.thumb.container.appendChild(timeContainer);
// Inject the whole thumb
this.player.elements.progress.appendChild(this.elements.thumb.container);
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', {
@@ -422,7 +443,9 @@ class PreviewThumbnails {
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;
@@ -463,7 +486,6 @@ class PreviewThumbnails {
const { urlPrefix } = this.thumbnails[0];
const thumbURL = urlPrefix + newThumbFilename;
const previewImage = new Image();
previewImage.src = thumbURL;
previewImage.onload = () => {
@@ -532,8 +554,11 @@ class PreviewThumbnails {
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);
const { height } = fitRatio(this.thumbAspectRatio, {
width: this.player.media.clientWidth,
height: this.player.media.clientHeight,
});
return height;
}
return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);
@@ -597,11 +622,9 @@ class PreviewThumbnails {
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;
@@ -618,9 +641,12 @@ class PreviewThumbnails {
// 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`;
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
@@ -632,9 +658,13 @@ class PreviewThumbnails {
// 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`;
// 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`;
}
}
+13 -13
View File
@@ -9,7 +9,7 @@ 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 loadScript from '../utils/load-script';
import { extend } from '../utils/objects';
import { format, stripHTML } from '../utils/strings';
import { setAspectRatio } from '../utils/style';
@@ -48,14 +48,14 @@ const vimeo = {
// Set intial ratio
setAspectRatio.call(this);
// Load the API if not already
// 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 API failed to load', error);
this.debug.warn('Vimeo SDK (player.js) failed to load', error);
});
} else {
vimeo.ready.call(this);
@@ -91,7 +91,6 @@ const vimeo = {
}
const id = parseId(source);
// Build an iframe
const iframe = createElement('iframe');
const src = format(player.config.urls.vimeo.iframe, id, params);
@@ -102,7 +101,6 @@ const vimeo = {
// Get poster, if already set
const { poster } = player;
// Inject the package
const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe);
@@ -198,12 +196,6 @@ const vimeo = {
.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, []);
}
});
},
});
@@ -259,7 +251,7 @@ const vimeo = {
.getVideoUrl()
.then(value => {
currentSrc = value;
controls.setDownloadLink.call(player);
controls.setDownloadUrl.call(player);
})
.catch(error => {
this.debug.warn(error);
@@ -281,7 +273,7 @@ const vimeo = {
// 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}`;
player.embed.ratio = [width, height];
setAspectRatio.call(this);
});
@@ -337,6 +329,14 @@ const vimeo = {
}
});
player.embed.on('bufferstart', () => {
triggerEvent.call(player, player.media, 'waiting');
});
player.embed.on('bufferend', () => {
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('play', () => {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
+42 -51
View File
@@ -7,8 +7,8 @@ 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';
@@ -52,73 +52,60 @@ const youtube = {
// Add embed class for responsive
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set aspect ratio
setAspectRatio.call(this);
// 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 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);
});
// Reference current global callback
const callback = window.onYouTubeIframeAPIReady;
// Set callback to process queue
window.onYouTubeIframeAPIReady = () => {
window.onYouTubeReadyCallbacks.forEach(callback => {
// Call global callback if set
if (is.function(callback)) {
callback();
});
}
youtube.ready.call(this);
};
// Load the SDK
loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
}
},
// Get the media title
getTitle(videoId) {
// Try via undocumented API method first
// This method disappears now and then though...
// https://github.com/sampotts/plyr/issues/709
if (is.function(this.embed.getVideoData)) {
const { title } = this.embed.getVideoData();
const url = format(this.config.urls.youtube.api, videoId);
if (is.empty(title)) {
this.config.title = title;
ui.setTitle.call(this);
return;
}
}
fetch(url)
.then(data => {
if (is.object(data)) {
const { title, height, width } = data;
// Or via Google API
const key = this.config.keys.google;
if (is.string(key) && !is.empty(key)) {
const url = format(this.config.urls.youtube.api, videoId, key);
// Set title
this.config.title = title;
ui.setTitle.call(this);
fetch(url)
.then(result => {
if (is.object(result)) {
this.config.title = result.items[0].snippet.title;
ui.setTitle.call(this);
}
})
.catch(() => {});
}
// Set aspect ratio
this.embed.ratio = [width, height];
}
setAspectRatio.call(this);
})
.catch(() => {
// Set aspect ratio
setAspectRatio.call(this);
});
},
// API ready
ready() {
const player = this;
// Ignore already setup (race condition)
const currentId = player.media.getAttribute('id');
const currentId = player.media && player.media.getAttribute('id');
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return;
}
@@ -134,25 +121,23 @@ const youtube = {
// Replace the <iframe> with a <div> due to YouTube API issues
const videoId = parseId(source);
const id = generateId(player.provider);
// Get poster, if already set
const { poster } = player;
// Replace media element
const container = createElement('div', { id, poster });
player.media = replaceElement(container, player.media);
// Id to poster wrapper
const posterSrc = format => `https://i.ytimg.com/vi/${videoId}/${format}default.jpg`;
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(posterSrc => {
.then(src => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!posterSrc.includes('maxres')) {
if (!src.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
}
})
@@ -431,6 +416,12 @@ const youtube = {
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;
}
+595
View File
@@ -0,0 +1,595 @@
// 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.
* Remarks: YouTube only. HTML5 will follow.
*/
quality: string;
/**
* 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;
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;
/**
* 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(
event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
callback: (this: this, event: Plyr.PlyrEvent) => void,
): void;
/**
* Add an event listener for the specified event once.
*/
once(
event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
callback: (this: this, event: Plyr.PlyrEvent) => void,
): void;
/**
* Remove an event listener for the specified event.
*/
off(
event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
callback: (this: this, event: Plyr.PlyrEvent) => void,
): void;
/**
* Check support for a mime type.
*/
supports(type: string): boolean;
/**
* Destroy lib instance
*/
destroy(): void;
}
declare namespace Plyr {
type MediaType = 'audio' | 'video';
type Provider = 'html5' | 'youtube' | 'vimeo';
type StandardEvent =
| 'progress'
| 'playing'
| 'play'
| 'pause'
| 'timeupdate'
| 'volumechange'
| 'seeking'
| 'seeked'
| 'ratechange'
| 'ended'
| 'enterfullscreen'
| 'exitfullscreen'
| 'captionsenabled'
| 'captionsdisabled'
| 'languagechange'
| 'controlshidden'
| 'controlsshown'
| 'ready';
type Html5Event =
| 'loadstart'
| 'loadeddata'
| 'loadedmetadata'
| 'canplay'
| 'canplaythrough'
| 'stalled'
| 'waiting'
| 'emptied'
| 'cuechange'
| 'error';
type YoutubeEvent = 'statechange' | 'qualitychange' | 'qualityrequested';
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.
*/
blankUrl?: 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;
}
interface QualityOptions {
default: string;
options: string[];
}
interface LoopOptions {
active: boolean;
}
interface AdOptions {
enabled: boolean;
publisherId: 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;
allowAudio?: boolean;
}
interface CaptionOptions {
active?: boolean;
language?: string;
update?: boolean;
}
interface StorageOptions {
enabled?: boolean;
key?: string;
}
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[];
}
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 };
}
interface Support {
api: boolean;
ui: boolean;
}
}
+76 -33
View File
@@ -1,6 +1,6 @@
// ==========================================================================
// Plyr
// plyr.js v3.5.3
// plyr.js v3.5.7
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
@@ -15,7 +15,7 @@ import Fullscreen from './fullscreen';
import Listeners from './listeners';
import media from './media';
import Ads from './plugins/ads';
import PreviewThumbnails from './plugins/previewThumbnails';
import PreviewThumbnails from './plugins/preview-thumbnails';
import source from './source';
import Storage from './storage';
import support from './support';
@@ -24,7 +24,8 @@ import { closest } from './utils/arrays';
import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';
import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
import is from './utils/is';
import loadSprite from './utils/loadSprite';
import loadSprite from './utils/load-sprite';
import { clamp } from './utils/numbers';
import { cloneDeep, extend } from './utils/objects';
import { getAspectRatio, reduceAspectRatio, setAspectRatio, validateRatio } from './utils/style';
import { parseUrl } from './utils/urls';
@@ -150,7 +151,6 @@ class Plyr {
// Set media type based on tag or data attribute
// Supported: video, audio, vimeo, youtube
const type = this.media.tagName.toLowerCase();
// Embed properties
let iframe = null;
let url = null;
@@ -323,27 +323,27 @@ class Plyr {
* Types and provider helpers
*/
get isHTML5() {
return Boolean(this.provider === providers.html5);
return this.provider === providers.html5;
}
get isEmbed() {
return Boolean(this.isYouTube || this.isVimeo);
return this.isYouTube || this.isVimeo;
}
get isYouTube() {
return Boolean(this.provider === providers.youtube);
return this.provider === providers.youtube;
}
get isVimeo() {
return Boolean(this.provider === providers.vimeo);
return this.provider === providers.vimeo;
}
get isVideo() {
return Boolean(this.type === types.video);
return this.type === types.video;
}
get isAudio() {
return Boolean(this.type === types.audio);
return this.type === types.audio;
}
/**
@@ -368,10 +368,10 @@ class Plyr {
*/
pause() {
if (!this.playing || !is.function(this.media.pause)) {
return;
return null;
}
this.media.pause();
return this.media.pause();
}
/**
@@ -411,10 +411,10 @@ class Plyr {
const toggle = is.boolean(input) ? input : !this.playing;
if (toggle) {
this.play();
} else {
this.pause();
return this.play();
}
return this.pause();
}
/**
@@ -441,7 +441,7 @@ class Plyr {
* @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
*/
rewind(seekTime) {
this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime);
this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;
}
/**
@@ -449,7 +449,7 @@ class Plyr {
* @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
*/
forward(seekTime) {
this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime);
this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;
}
/**
@@ -513,7 +513,6 @@ class Plyr {
get duration() {
// Faux duration set via config
const fauxDuration = parseFloat(this.config.duration);
// Media duration can be NaN or Infinity before the media has loaded
const realDuration = (this.media || {}).duration;
const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;
@@ -661,24 +660,17 @@ class Plyr {
speed = this.config.speed.selected;
}
// Set min/max
if (speed < 0.1) {
speed = 0.1;
}
if (speed > 2.0) {
speed = 2.0;
}
if (!this.config.speed.options.includes(speed)) {
this.debug.warn(`Unsupported speed (${speed})`);
return;
}
// Clamp to min/max
const { minimumSpeed: min, maximumSpeed: max } = this;
speed = clamp(speed, min, max);
// Update config
this.config.speed.selected = speed;
// Set media speed
this.media.playbackRate = speed;
setTimeout(() => {
this.media.playbackRate = speed;
}, 0);
}
/**
@@ -688,6 +680,42 @@ class Plyr {
return Number(this.media.playbackRate);
}
/**
* Get the minimum allowed speed
*/
get minimumSpeed() {
if (this.isYouTube) {
// https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
return Math.min(...this.options.speed);
}
if (this.isVimeo) {
// https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
return 0.5;
}
// https://stackoverflow.com/a/32320020/1191319
return 0.0625;
}
/**
* Get the maximum allowed speed
*/
get maximumSpeed() {
if (this.isYouTube) {
// https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
return Math.max(...this.options.speed);
}
if (this.isVimeo) {
// https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
return 2;
}
// https://stackoverflow.com/a/32320020/1191319
return 16;
}
/**
* Set playback quality
* Currently HTML5 & YouTube only
@@ -823,6 +851,19 @@ class Plyr {
return is.url(download) ? download : this.source;
}
/**
* Set the download URL
*/
set download(input) {
if (!is.url(input)) {
return;
}
this.config.urls.download = input;
controls.setDownloadUrl.call(this);
}
/**
* Set the poster image for a video
* @param {String} input - the URL for the new poster image
@@ -851,6 +892,10 @@ class Plyr {
* Get the current aspect ratio in use
*/
get ratio() {
if (!this.isVideo) {
return null;
}
const ratio = reduceAspectRatio(getAspectRatio.call(this));
return is.array(ratio) ? ratio.join(':') : ratio;
@@ -998,10 +1043,8 @@ class Plyr {
if (this.supported.ui && !this.isAudio) {
// Get state before change
const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);
// Negate the argument if not undefined since adding the class to hides the controls
const force = typeof toggle === 'undefined' ? undefined : !toggle;
// Apply and get updated state
const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);
+2 -1
View File
@@ -1,12 +1,13 @@
// ==========================================================================
// Plyr Polyfilled Build
// plyr.js v3.5.3
// plyr.js v3.5.7
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import 'custom-event-polyfill';
import 'url-polyfill';
import Plyr from './plyr';
export default Plyr;
+30 -15
View File
@@ -10,7 +10,7 @@ import { getElement, toggleClass } from './utils/elements';
import { ready, triggerEvent } from './utils/events';
import i18n from './utils/i18n';
import is from './utils/is';
import loadImage from './utils/loadImage';
import loadImage from './utils/load-image';
const ui = {
addStyleHook() {
@@ -67,15 +67,15 @@ const ui = {
// Reset mute state
this.muted = null;
// Reset speed
this.speed = null;
// Reset loop state
this.loop = null;
// Reset quality setting
this.quality = null;
// Reset speed
this.speed = null;
// Reset volume display
controls.updateVolume.call(this);
@@ -198,7 +198,9 @@ const ui = {
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
ui.togglePoster.call(this, true);
return poster;
})
);
@@ -213,7 +215,8 @@ const ui = {
// Set state
Array.from(this.elements.buttons.play || []).forEach(target => {
target.pressed = this.playing;
Object.assign(target, { pressed: this.playing });
target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
});
// Only update controls on non timeupdate events
@@ -233,25 +236,37 @@ const ui = {
clearTimeout(this.timers.loading);
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => {
// Update progress bar loading class state
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
this.timers.loading = setTimeout(
() => {
// Update progress bar loading class state
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Update controls visibility
ui.toggleControls.call(this);
}, this.loading ? 250 : 0);
// Update controls visibility
ui.toggleControls.call(this);
},
this.loading ? 250 : 0,
);
},
// Toggle controls based on state and `force` argument
toggleControls(force) {
const { controls } = this.elements;
const { controls: controlsElement } = this.elements;
if (controls && this.config.hideControls) {
if (controlsElement && this.config.hideControls) {
// Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
const recentTouchSeek = (this.touch && this.lastSeekTime + 2000 > Date.now());
const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover || recentTouchSeek));
this.toggleControls(
Boolean(
force ||
this.loading ||
this.paused ||
controlsElement.pressed ||
controlsElement.hover ||
recentTouchSeek,
),
);
}
},
};
+10 -6
View File
@@ -2,7 +2,6 @@
// Animation utils
// ==========================================================================
import { toggleHidden } from './elements';
import is from './is';
export const transitionEndEvent = (() => {
@@ -21,14 +20,19 @@ export const transitionEndEvent = (() => {
})();
// Force repaint of element
export function repaint(element) {
export function repaint(element, delay) {
setTimeout(() => {
try {
toggleHidden(element, true);
element.offsetHeight; // eslint-disable-line
toggleHidden(element, false);
// eslint-disable-next-line no-param-reassign
element.hidden = true;
// eslint-disable-next-line no-unused-expressions
element.offsetHeight;
// eslint-disable-next-line no-param-reassign
element.hidden = false;
} catch (e) {
// Do nothing
}
}, 0);
}, delay);
}
+8
View File
@@ -21,3 +21,11 @@ export function closest(array, value) {
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
}
export function fillRange(start, end, step = 1) {
const len = Math.floor((end - start) / step) + 1;
return Array(len)
.fill()
.map((_, idx) => start + idx * step);
}
+12 -51
View File
@@ -2,8 +2,8 @@
// Element utils
// ==========================================================================
import { toggleListener } from './events';
import is from './is';
import { extend } from './objects';
// Wrap an element
export function wrap(elements, wrapper) {
@@ -16,7 +16,6 @@ export function wrap(elements, wrapper) {
.reverse()
.forEach((element, index) => {
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
// Cache the current parent and sibling.
const parent = element.parentNode;
const sibling = element.nextSibling;
@@ -137,30 +136,28 @@ export function getAttributesFromSelector(sel, existingAttributes) {
}
const attributes = {};
const existing = existingAttributes;
const existing = extend({}, existingAttributes);
sel.split(',').forEach(s => {
// Remove whitespace
const selector = s.trim();
const className = selector.replace('.', '');
const stripped = selector.replace(/[[\]]/g, '');
// Get the parts and value
const parts = stripped.split('=');
const key = parts[0];
const [key] = parts;
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
// Get the first character
const start = selector.charAt(0);
switch (start) {
case '.':
// Add to existing classname
if (is.object(existing) && is.string(existing.class)) {
existing.class += ` ${className}`;
if (is.string(existing.class)) {
attributes.class = `${existing.class} ${className}`;
} else {
attributes.class = className;
}
attributes.class = className;
break;
case '#':
@@ -179,7 +176,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {
}
});
return attributes;
return extend(existing, attributes);
}
// Toggle hidden
@@ -194,11 +191,8 @@ export function toggleHidden(element, hidden) {
hide = !element.hidden;
}
if (hide) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
// eslint-disable-next-line no-param-reassign
element.hidden = hide;
}
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
@@ -233,14 +227,14 @@ export function matches(element, selector) {
return Array.from(document.querySelectorAll(selector)).includes(this);
}
const matches =
const method =
prototype.matches ||
prototype.webkitMatchesSelector ||
prototype.mozMatchesSelector ||
prototype.msMatchesSelector ||
match;
return matches.call(element, selector);
return method.call(element, selector);
}
// Find all elements
@@ -253,39 +247,6 @@ export function getElement(selector) {
return this.elements.container.querySelector(selector);
}
// Trap focus inside container
export function trapFocus(element = null, toggle = false) {
if (!is.element(element)) {
return;
}
const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
const first = focusable[0];
const last = focusable[focusable.length - 1];
const trap = event => {
// Bail if not tab key or not fullscreen
if (event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element
const focused = document.activeElement;
if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used
first.focus();
event.preventDefault();
} else if (focused === first && event.shiftKey) {
// Move focus to last element that can be tabbed if Shift is used
last.focus();
event.preventDefault();
}
};
toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
}
// Set focus and tab focus class
export function setFocus(element = null, tabFocus = false) {
if (!is.element(element)) {
+1 -4
View File
@@ -35,7 +35,6 @@ export function toggleListener(element, event, callback, toggle = false, passive
// Allow multiple events
const events = event.split(' ');
// Build options
// Default to just the capture boolean for browsers with no passive listener support
let options = capture;
@@ -91,9 +90,7 @@ export function triggerEvent(element, type = '', bubbles = false, detail = {}) {
// Create and dispatch the event
const event = new CustomEvent(type, {
bubbles,
detail: Object.assign({}, detail, {
plyr: this,
}),
detail: { ...detail, plyr: this,},
});
// Dispatch the event
+2 -2
View File
@@ -36,8 +36,8 @@ const i18n = {
'{title}': config.title,
};
Object.entries(replace).forEach(([key, value]) => {
string = replaceAll(string, key, value);
Object.entries(replace).forEach(([k, v]) => {
string = replaceAll(string, k, v);
});
return string;
@@ -15,10 +15,10 @@ export default function loadSprite(url, id) {
const prefix = 'cache';
const hasId = is.string(id);
let isCached = false;
const exists = () => document.getElementById(id) !== null;
const update = (container, data) => {
// eslint-disable-next-line no-param-reassign
container.innerHTML = data;
// Check again incase of race condition
@@ -33,7 +33,6 @@ export default function loadSprite(url, id) {
// Only load once if ID set
if (!hasId || !exists()) {
const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
container.setAttribute('hidden', '');
+17
View File
@@ -0,0 +1,17 @@
/**
* Returns a number whose value is limited to the given range.
*
* Example: limit the output of this computation to between 0 and 255
* (x * 255).clamp(0, 255)
*
* @param {Number} input
* @param {Number} min The lower boundary of the output range
* @param {Number} max The upper boundary of the output range
* @returns A number in the range [min, max]
* @type Number
*/
export function clamp(input = 0, min = 0, max = 255) {
return Math.min(Math.max(input, min), max);
}
export default { clamp };
+13 -14
View File
@@ -27,15 +27,8 @@ export function reduceAspectRatio(ratio) {
}
export function getAspectRatio(input) {
const parse = ratio => {
if (!validateRatio(ratio)) {
return null;
}
return ratio.split(':').map(Number);
};
// Provided ratio
const parse = ratio => (validateRatio(ratio) ? ratio.split(':').map(Number) : null);
// Try provided ratio
let ratio = parse(input);
// Get from config
@@ -44,8 +37,14 @@ export function getAspectRatio(input) {
}
// Get from embed
if (ratio === null && !is.empty(this.embed) && is.string(this.embed.ratio)) {
ratio = parse(this.embed.ratio);
if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {
({ ratio } = this.embed);
}
// Get from HTML5 video
if (ratio === null && this.isHTML5) {
const { videoWidth, videoHeight } = this.media;
ratio = reduceAspectRatio([videoWidth, videoHeight]);
}
return ratio;
@@ -57,12 +56,12 @@ export function setAspectRatio(input) {
return {};
}
const { wrapper } = this.elements;
const ratio = getAspectRatio.call(this, input);
const [w, h] = is.array(ratio) ? ratio : [0, 0];
const padding = (100 / w) * h;
this.elements.wrapper.style.paddingBottom = `${padding}%`;
wrapper.style.paddingBottom = `${padding}%`;
// For Vimeo we have an extra <div> to hide the standard controls and UI
if (this.isVimeo && this.supported.ui) {
@@ -70,7 +69,7 @@ export function setAspectRatio(input) {
const offset = (height - padding) / (height / 50);
this.media.style.transform = `translateY(-${offset}%)`;
} else if (this.isHTML5) {
this.elements.wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null);
wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null);
}
return { padding, ratio };
+1 -2
View File
@@ -13,12 +13,11 @@ export const getSeconds = value => Math.trunc(value % 60, 10);
export function formatTime(time = 0, displayHours = false, inverted = false) {
// Bail if the value isn't a number
if (!is.number(time)) {
return formatTime(null, displayHours, inverted);
return formatTime(undefined, displayHours, inverted);
}
// Format time component to add leading zero
const format = value => `0${value}`.slice(-2);
// Breakdown to hours, mins, secs
let hours = getHours(time);
const mins = getMinutes(time);
+8 -5
View File
@@ -5,24 +5,27 @@
// Base
.plyr {
@include plyr-font-smoothing($plyr-font-smoothing);
align-items: center;
direction: ltr;
display: flex;
font-family: $plyr-font-family;
font-variant-numeric: tabular-nums; // Force monosace-esque number widths
font-weight: $plyr-font-weight-regular;
height: 100%;
line-height: $plyr-line-height;
max-width: 100%;
min-width: 200px;
position: relative;
text-shadow: none;
transition: box-shadow 0.3s ease;
z-index: 0; // Force any border radius
// Media elements
video,
audio {
border-radius: inherit;
height: auto;
vertical-align: middle;
audio,
iframe {
display: block;
height: 100%;
width: 100%;
}
+7
View File
@@ -0,0 +1,7 @@
// --------------------------------------------------------------
// Audio styles
// --------------------------------------------------------------
.plyr--audio {
display: block;
}
-5
View File
@@ -63,10 +63,6 @@ a.plyr__control {
// Video control
.plyr--video .plyr__control {
svg {
filter: drop-shadow(0 1px 1px rgba(#000, 0.15));
}
// Hover and tab focus
&.plyr__tab-focus,
&:hover,
@@ -81,7 +77,6 @@ a.plyr__control {
background: rgba($plyr-video-control-bg-hover, 0.8);
border: 0;
border-radius: 100%;
box-shadow: 0 1px 1px rgba(#000, 0.15);
color: $plyr-video-control-color;
display: none;
left: 50%;
+25 -31
View File
@@ -14,42 +14,39 @@
justify-content: flex-end;
text-align: center;
.plyr__progress__container {
flex: 1;
min-width: 0; // Fix for Edge issue where content would overflow
}
// Spacing
> .plyr__control,
.plyr__progress,
.plyr__time,
.plyr__menu,
.plyr__volume {
margin-left: ($plyr-control-spacing / 2);
}
.plyr__controls__item {
margin-left: ($plyr-control-spacing / 4);
.plyr__menu + .plyr__control,
> .plyr__control + .plyr__menu,
> .plyr__control + .plyr__control,
.plyr__progress + .plyr__control {
margin-left: floor($plyr-control-spacing / 4);
}
&:first-child {
margin-left: 0;
margin-right: auto;
}
> .plyr__control:first-child,
> .plyr__control:first-child + [data-plyr='pause'] {
margin-left: 0;
margin-right: auto;
&.plyr__progress__container {
padding-left: ($plyr-control-spacing / 4);
}
&.plyr__time {
padding: 0 ($plyr-control-spacing / 2);
}
&.plyr__progress__container:first-child,
&.plyr__time:first-child,
&.plyr__time + .plyr__time {
padding-left: 0;
}
}
// Hide empty controls
&:empty {
display: none;
}
@media (min-width: $plyr-bp-sm) {
> .plyr__control,
.plyr__menu,
.plyr__progress,
.plyr__time,
.plyr__volume {
margin-left: $plyr-control-spacing;
}
}
}
// Audio controls
@@ -62,10 +59,7 @@
// Video controls
.plyr--video .plyr__controls {
background: linear-gradient(
rgba($plyr-video-controls-bg, 0),
rgba($plyr-video-controls-bg, 0.7)
);
background: linear-gradient(rgba($plyr-video-controls-bg, 0), rgba($plyr-video-controls-bg, 0.7));
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;
bottom: 0;
+8 -7
View File
@@ -2,18 +2,19 @@
// Playback progress
// --------------------------------------------------------------
// Offset the range thumb in order to be able to calculate the relative progress (#954)
$plyr-progress-offset: $plyr-range-thumb-height;
.plyr__progress {
flex: 1;
left: $plyr-range-thumb-height / 2;
margin-right: $plyr-range-thumb-height;
left: $plyr-progress-offset / 2;
margin-right: $plyr-progress-offset;
position: relative;
input[type='range'],
&__buffer {
margin-left: -($plyr-range-thumb-height / 2);
margin-right: -($plyr-range-thumb-height / 2);
// Offset the range thumb in order to be able to calculate the relative progress (#954)
width: calc(100% + #{$plyr-range-thumb-height});
margin-left: -($plyr-progress-offset / 2);
margin-right: -($plyr-progress-offset / 2);
width: calc(100% + #{$plyr-progress-offset});
}
input[type='range'] {
+3 -7
View File
@@ -14,11 +14,10 @@
.plyr__video-wrapper {
background: #000;
border-radius: inherit;
height: 100%;
margin: auto;
overflow: hidden;
position: relative;
// Require z-index to force border-radius
z-index: 0;
width: 100%;
}
// Default to 16:9 ratio but this is set by JavaScript based on config
@@ -33,12 +32,9 @@ $embed-padding: ((100 / 16) * 9);
.plyr__video-embed iframe,
.plyr__video-wrapper--fixed-ratio video {
border: 0;
height: 100%;
left: 0;
position: absolute;
top: 0;
user-select: none;
width: 100%;
}
// If the full custom UI is supported
+1 -14
View File
@@ -5,11 +5,11 @@
.plyr__volume {
align-items: center;
display: flex;
flex: 1;
position: relative;
input[type='range'] {
margin-left: ($plyr-control-spacing / 2);
margin-right: ($plyr-control-spacing / 2);
position: relative;
z-index: 2;
}
@@ -22,16 +22,3 @@
max-width: 110px;
}
}
// Hide sound controls on iOS
// It's not supported to change volume using JavaScript:
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
.plyr--is-ios .plyr__volume {
display: none !important;
}
// Vimeo has no toggle mute method so hide mute button
// https://github.com/vimeo/player.js/issues/236#issuecomment-384663183
.plyr--is-ios.plyr--vimeo [data-plyr='mute'] {
display: none !important;
}
+2 -1
View File
@@ -62,12 +62,13 @@
.plyr__video-wrapper {
height: 100%;
width: 100%;
position: static;
}
// Vimeo requires some different styling
&.plyr--vimeo .plyr__video-wrapper {
height: 0;
position: relative;
top: 50%;
transform: translateY(-50%);
}
+1 -1
View File
@@ -23,7 +23,7 @@
// The countdown label
&::after {
background: rgba($plyr-color-gunmetal, 0.8);
background: rgba($plyr-color-gray-9, 0.8);
border-radius: 2px;
bottom: $plyr-control-spacing;
color: #fff;
@@ -7,7 +7,7 @@ $plyr-preview-bg: $plyr-tooltip-bg !default;
$plyr-preview-radius: $plyr-tooltip-radius !default;
$plyr-preview-shadow: $plyr-tooltip-shadow !default;
$plyr-preview-arrow-size: $plyr-tooltip-arrow-size !default;
$plyr-preview-image-bg: $plyr-color-heather !default;
$plyr-preview-image-bg: $plyr-color-gray-2 !default;
$plyr-preview-time-font-size: $plyr-font-size-time !default;
$plyr-preview-time-padding: 3px 6px !default;
$plyr-preview-time-bg: rgba(0, 0, 0, 0.55);
+2 -1
View File
@@ -25,6 +25,7 @@
@import 'base';
@import 'components/audio';
@import 'components/badges';
@import 'components/captions';
@import 'components/control';
@@ -41,7 +42,7 @@
@import 'states/fullscreen';
@import 'plugins/ads';
@import 'plugins/previewThumbnails';
@import 'plugins/preview-thumbnails';
@import 'utils/animation';
@import 'utils/hidden';
+1 -1
View File
@@ -2,5 +2,5 @@
// Badges
// ==========================================================================
$plyr-badge-bg: $plyr-color-fiord !default;
$plyr-badge-bg: $plyr-color-gray-7 !default;
$plyr-badge-color: #fff !default;
+13 -5
View File
@@ -2,8 +2,16 @@
// Colors
// ==========================================================================
$plyr-color-main: #1aafff !default;
$plyr-color-gunmetal: #2f343d !default;
$plyr-color-fiord: #4f5b5f !default;
$plyr-color-lynch: #6b7d85 !default;
$plyr-color-heather: #b7c5cd !default;
$plyr-color-main: hsl(198, 100%, 50%) !default;
// Grayscale
$plyr-color-gray-9: hsl(210, 15%, 16%);
$plyr-color-gray-8: lighten($plyr-color-gray-9, 9%);
$plyr-color-gray-7: lighten($plyr-color-gray-8, 9%);
$plyr-color-gray-6: lighten($plyr-color-gray-7, 9%);
$plyr-color-gray-5: lighten($plyr-color-gray-6, 9%);
$plyr-color-gray-4: lighten($plyr-color-gray-5, 9%);
$plyr-color-gray-3: lighten($plyr-color-gray-4, 9%);
$plyr-color-gray-2: lighten($plyr-color-gray-3, 9%);
$plyr-color-gray-1: lighten($plyr-color-gray-2, 9%);
$plyr-color-gray-0: lighten($plyr-color-gray-1, 9%);
+1 -1
View File
@@ -13,6 +13,6 @@ $plyr-video-control-color-hover: #fff !default;
$plyr-video-control-bg-hover: $plyr-color-main !default;
$plyr-audio-controls-bg: #fff !default;
$plyr-audio-control-color: $plyr-color-fiord !default;
$plyr-audio-control-color: $plyr-color-gray-7 !default;
$plyr-audio-control-color-hover: #fff !default;
$plyr-audio-control-bg-hover: $plyr-color-main !default;
+2 -2
View File
@@ -3,8 +3,8 @@
// ==========================================================================
$plyr-menu-bg: rgba(#fff, 0.9) !default;
$plyr-menu-color: $plyr-color-fiord !default;
$plyr-menu-color: $plyr-color-gray-7 !default;
$plyr-menu-arrow-size: 6px !default;
$plyr-menu-border-color: $plyr-color-heather !default;
$plyr-menu-border-color: $plyr-color-gray-2 !default;
$plyr-menu-border-shadow-color: #fff !default;
$plyr-menu-shadow: 0 1px 2px rgba(#000, 0.15) !default;
+2 -2
View File
@@ -4,8 +4,8 @@
// Loading
$plyr-progress-loading-size: 25px !default;
$plyr-progress-loading-bg: rgba($plyr-color-gunmetal, 0.6) !default;
$plyr-progress-loading-bg: rgba($plyr-color-gray-9, 0.6) !default;
// Buffered
$plyr-video-progress-buffered-bg: rgba(#fff, 0.25) !default;
$plyr-audio-progress-buffered-bg: rgba($plyr-color-heather, 0.66) !default;
$plyr-audio-progress-buffered-bg: rgba($plyr-color-gray-2, 0.66) !default;
+1 -1
View File
@@ -9,7 +9,7 @@ $plyr-range-thumb-active-shadow-width: 3px !default;
$plyr-range-thumb-height: 13px !default;
$plyr-range-thumb-bg: #fff !default;
$plyr-range-thumb-border: 2px solid transparent !default;
$plyr-range-thumb-shadow: 0 1px 1px rgba(#000, 0.15), 0 0 0 1px rgba($plyr-color-gunmetal, 0.2) !default;
$plyr-range-thumb-shadow: 0 1px 1px rgba(#000, 0.15), 0 0 0 1px rgba($plyr-color-gray-9, 0.2) !default;
// Track
$plyr-range-track-height: 5px !default;
+1 -1
View File
@@ -3,7 +3,7 @@
// ==========================================================================
$plyr-tooltip-bg: rgba(#fff, 0.9) !default;
$plyr-tooltip-color: $plyr-color-fiord !default;
$plyr-tooltip-color: $plyr-color-gray-7 !default;
$plyr-tooltip-padding: ($plyr-control-spacing / 2) !default;
$plyr-tooltip-arrow-size: 4px !default;
$plyr-tooltip-radius: 3px !default;
+1 -1
View File
@@ -24,8 +24,8 @@
// Fallback for unsupported browsers
.plyr--fullscreen-fallback {
@include plyr-fullscreen-active();
bottom: 0;
display: block;
left: 0;
position: fixed;
right: 0;
+3659 -2286
View File
File diff suppressed because it is too large Load Diff