Compare commits

...

273 Commits

Author SHA1 Message Date
Sam Potts 2c8a337f26 v3.4.7
-   Fix for Vimeo fullscreen with non native aspect ratios (fixes #854)
2018-11-08 23:34:10 +11:00
Sam Potts c24e52d97d Package updates 2018-11-08 23:29:54 +11:00
Sam Potts 574f40949c Merge branch 'master' into develop 2018-11-08 23:19:57 +11:00
Sam Potts cf3848fbd5 Merge branch 'develop' of github.com:sampotts/plyr into develop 2018-11-08 23:19:26 +11:00
Sam Potts a19ad69038 Fix for Vimeo fullscreen with non 16:9 aspect ratios 2018-11-08 23:19:11 +11:00
Sam Potts d6f20e2756 Package upgrades 2018-11-08 23:18:23 +11:00
Sam Potts e2fc20ca76 Styling tweaks 2018-11-08 23:18:04 +11:00
Sam Potts 37c3f7109d Additional listener for checking for audio tracks 2018-11-08 23:17:44 +11:00
Sam Potts 99d5211a33 Merge pull request #1247 from danielcgold/patch-1
[Edit README] Halfhalftravel uses Plyr!
2018-11-06 10:17:55 +11:00
Dan Gold b97f143195 Halfhalftravel uses Plyr!
https://www.halfhalftravel.com/events/medellin-photo-walk.html
https://www.halfhalftravel.com/travel-guides/how-to-travel-to-santa-fe-de-antioquia-colombia.html
2018-11-05 10:57:23 -05:00
Sam Potts e8da4326b6 Prevent scroll on focus 2018-11-03 21:17:46 +11:00
Sam Potts 67f908aa8d Load media after UI is built 2018-11-03 21:17:32 +11:00
Sam Potts 65eb5c1b8b Fix support check 2018-11-03 21:16:40 +11:00
Sam Potts 7d484c6e09 Merge pull request #1232 from tocsinde/patch-1
Readme: Add missing annotation to PIP support
2018-10-26 08:37:33 +11:00
Stephan Fischer 8252e13eb9 Readme: Add missing annotation to PIP support
PIP only works on HTML videos, so I added the number of the (already existing) annotation.
2018-10-25 23:35:20 +02:00
Sam Potts 1a9b860e68 v3.4.6
-   Added picture-in-picture support for Chrome 70+
-   Fixed issue with versioning the SVG sprite in the gulp build script
2018-10-25 09:44:40 +11:00
Sam Potts cede7d0f35 Fix gulp build 2018-10-25 09:21:37 +11:00
Sam Potts fe26d383f1 Added support for picture-in-picture in Chrome 2018-10-25 09:17:15 +11:00
Sam Potts df4bc268dc Merge branch 'master' into develop 2018-10-25 00:14:14 +11:00
Sam Potts e49da6c13f v3.4.5 2018-10-24 23:04:18 +11:00
Sam Potts 67b7262764 Revert PR #1211 2018-10-24 23:00:54 +11:00
Sam Potts 88528ef979 Merge pull request #1197 from TechGuard/fix-html5-quality-settings
Fix html5 quality settings
2018-10-24 22:39:34 +11:00
Sam Potts b6175b1ca9 Merge branch 'develop' into fix-html5-quality-settings 2018-10-24 22:39:10 +11:00
Sam Potts aa20ebaa9c Merge pull request #1211 from melbahja/develop
duration after changing video quality
2018-10-24 22:37:49 +11:00
Sam Potts 779e45c11b Merge branch 'master' into develop 2018-10-24 22:32:55 +11:00
Sam Potts 5d5a6eabaa Merge branch 'develop' of github.com:sampotts/plyr into develop 2018-10-24 22:31:47 +11:00
Sam Potts 03c9b53232 Allow custom download URL (for streaming, etc) 2018-10-24 22:31:35 +11:00
Sam Potts ebaded66b4 Package updates 2018-10-24 22:31:16 +11:00
Sam Potts c232eb2478 Fix SVG issue for older browsers (fixes #1191) 2018-10-24 22:30:41 +11:00
Sam Potts 7559cc6897 Merge pull request #1226 from jamesoflol/dont-hide-mobile-controls-immediately
Prevent immediate hiding of controls on mobile
2018-10-23 11:09:11 +11:00
James 69d0d6d7ee Prevent immediate hiding of controls on mobile 2018-10-23 10:08:46 +11:00
Sam Potts 3e9336b15d Merge pull request #1217 from epalmans/master
typo
2018-10-17 16:55:49 +11:00
e_palm 5c78ecc15d typo 2018-10-16 14:48:10 +02:00
Mohamed Elbahja 06db3f702d Update plyr.js 2018-10-13 13:23:42 +01:00
Mohamed Elbahja a2a82a96a6 fix: continue with the current duration after changing video quality 2018-10-13 12:59:59 +01:00
Robin van Nunen a86bbae851 Only save quality setting when it's updated by the user. Fixes bug in html5 player where it would override the settings if the current video does not support the given quality. 2018-09-29 21:23:10 +02:00
Sam Potts fac134dd95 Added download button 2018-09-28 00:42:42 +10:00
Sam Potts 515ae32160 Moved hardcoded resources to i18n 2018-09-28 00:30:27 +10:00
Sam Potts df8f040795 Remove link styles from anchor controls 2018-09-28 00:29:59 +10:00
Sam Potts 64a23992f0 SVG cleanup 2018-09-28 00:29:42 +10:00
Sam Potts f5baff6e6b Merge pull request #1192 from jamesoflol/more-mobile-touch-issues
Don't hide controls on focusout event
2018-09-27 21:33:37 +10:00
James 8bdd90a2a8 Don't hide controls on focusout event
It was immediately hiding controls on some touch-enabled devices. It will now also wait 4s to close after tabbing out, instead of immediately.
2018-09-26 14:48:10 +10:00
Sam Potts 5536e97482 Typo 2018-09-25 23:48:33 +10:00
Sam Potts de47071256 v3.4.4
-   Fixed issue with double binding for `click` and `touchstart` for `clickToPlay` option
-   Improved "faux" fullscreen on iPhone X/XS phones with notch
-   Babel 7 upgrade (which reduced the polyfilled build by ~10kb!)
2018-09-25 23:36:50 +10:00
Sam Potts 87072cb690 Clean up 2018-09-25 23:29:43 +10:00
Sam Potts d9565e9250 Improved fullscreen on iPhone X etc 2018-09-25 23:29:32 +10:00
Sam Potts f80b568e67 Reverted large pause button 2018-09-25 23:07:48 +10:00
Sam Potts 7fed689f9a Yarn lock file 2018-09-25 22:25:46 +10:00
Sam Potts 3f48df8f10 Remove babel-polyfill in favour of core-js 2018-09-25 22:25:35 +10:00
Sam Potts cc55092ca6 Babel upgrades 2018-09-25 22:25:04 +10:00
Sam Potts 3331d9d01d Package upgrades 2018-09-25 20:46:58 +10:00
Sam Potts 62d80e6b76 Fix touch vs click issue 2018-09-25 20:43:09 +10:00
Sam Potts 7dc4d9cd22 v3.4.3 2018-08-14 12:16:34 +10:00
Sam Potts 8fb8ae1260 Merge pull request #1163 from sampotts/develop
Fix bug with nodeList for play buttons
2018-08-14 12:14:58 +10:00
Sam Potts 90304369f4 Fix watch 2018-08-14 12:13:16 +10:00
Sam Potts 922456c46c Fix for nodeList as buttons 2018-08-14 12:13:00 +10:00
Sam Potts eaeccd66ae v3.4.2 2018-08-14 11:17:33 +10:00
Sam Potts 7a43649c13 Fix play/pause button state 2018-08-14 11:17:27 +10:00
Sam Potts 525bbf313e v3.4.1 2018-08-14 09:18:09 +10:00
Sam Potts cfaebe9bf2 Fix for controls missing (fixes #1161) 2018-08-14 09:17:58 +10:00
Sam Potts b57b7b2153 v3.4.0
-   Accessibility improvements (see #905)
-   Improvements to the way the controls work on iOS
-   Demo code clean up
-   YouTube quality selection removed due to their poor support for it. As a result, the `qualityrequested` event has been removed
-   Controls spacing improvements
-   Fix for pressed property missing with custom controls (Fixes #1062)
-   Fix #1153: Captions language fallback (thanks @friday)
-   Fix for setting pressed property of undefined (Fixes #1102)
2018-08-14 00:02:01 +10:00
Sam Potts 48bf368316 Merge pull request #1160 from sampotts/develop
v3.4.0
2018-08-14 00:00:24 +10:00
Sam Potts 8f94ce86a0 Merge branch 'master' into develop
# Conflicts:
#	readme.md
2018-08-13 23:59:19 +10:00
Sam Potts 10a9cf08f1 Changelog 2018-08-13 23:57:46 +10:00
Sam Potts 286d0d1794 Fix for pressed property missing with custom controls (Fixes #1062) 2018-08-13 23:52:10 +10:00
Sam Potts 95f6fa2731 Fix for setting pressed property of undefined (Fixes #1102) 2018-08-13 23:46:58 +10:00
Sam Potts 1aeef81288 Controls spacing improvements 2018-08-13 23:43:22 +10:00
Sam Potts 211ad6c8f5 Removed YouTube quality controls 2018-08-13 23:43:08 +10:00
Sam Potts 468b20d227 Moved mute button inside plyr__volume 2018-08-13 23:42:12 +10:00
Sam Potts f6bc42c2bc Fix IE11 issue in demo 2018-08-13 23:03:08 +10:00
Sam Potts 2c01b8ba76 Yarn lock file 2018-08-13 23:02:31 +10:00
Sam Potts 4e1df8677f Fix tooltip alignment 2018-08-13 23:02:14 +10:00
Sam Potts 6953a12e2a Set background color for video 2018-08-13 23:01:56 +10:00
Sam Potts 1d0db89194 Update wrong reference in docs 2018-08-13 23:01:38 +10:00
Sam Potts 297f297d18 Moved i18n to utils 2018-08-13 21:39:16 +10:00
Sam Potts 059205c378 Package updates 2018-08-13 21:39:02 +10:00
Sam Potts f94e53ffb1 Merge pull request #1158 from friday/1153
Fix #1153: Captions language fallback
2018-08-13 09:24:25 +10:00
Albin Larsson a4f1fdec5d Fix #1153: Captions language fallback 2018-08-12 20:12:22 +02:00
Sam Potts 75374eb154 Merge pull request #1147 from jamesoflol/fix-ios-fullscreen-while-stopped
Remove 'video is playing' requirement for iosNative fullscreen
2018-08-05 22:46:58 +10:00
Sam Potts 3ad118c026 3.4.0-beta.2 2018-08-05 22:43:35 +10:00
Sam Potts 0bc6b1f1b3 Fix issue where enter key wasn’t setting focus correctly 2018-08-05 22:41:21 +10:00
Sam Potts 4ea458e1a3 Rounded aria-valuetext to 1 decimal place 2018-08-05 21:48:42 +10:00
Sam Potts aacb172017 Removed aria-labelled-by 2018-08-05 21:48:21 +10:00
James dbf768b1bd Remove 'video is playing' requirement for iosNative fullscreen 2018-08-03 09:58:51 +10:00
Sam Potts b96fcfc8ac v3.4.0-beta.1 2018-08-02 00:55:48 +10:00
Sam Potts 18b4d26bee Merge pull request #1142 from sampotts/a11y-improvements
A11y improvements
2018-08-02 00:47:57 +10:00
Sam Potts 7f4b74e2d4 Fix for hover over iframed players not showing controls 2018-08-02 00:47:03 +10:00
Sam Potts a8f8486cf4 Merge pull request #1143 from mhluska/patch-1
Fix Readme typo (Patron -> Patreon)
2018-08-01 15:09:00 +10:00
Maros Hluska a343e58e53 Fix Readme typo (Patron -> Patreon) 2018-08-01 12:08:02 +07:00
Sam Potts 0892d69ba2 Handle race condition for ads lib loading after source change 2018-08-01 13:56:49 +10:00
Sam Potts ba511b51c7 Box shadow fix for range track 2018-08-01 13:00:51 +10:00
Sam Potts e090581913 Ads on dev or prod only 2018-08-01 11:49:42 +10:00
Sam Potts aaa56caa9c Only focus button if menu wasn’t hidden already 2018-08-01 01:38:57 +10:00
Sam Potts c8db1e55dd Escape closes menu 2018-08-01 01:26:15 +10:00
Sam Potts 58079393e6 Build 2018-08-01 00:58:27 +10:00
Sam Potts 0b44f2d897 Demo config 2018-08-01 00:57:45 +10:00
Sam Potts 2371619486 Linting 2018-08-01 00:56:44 +10:00
Sam Potts 13a54b5dbe Merge branch 'develop' into a11y-improvements
# Conflicts:
#	src/js/controls.js
2018-08-01 00:46:26 +10:00
Sam Potts fa0861ff2e Merge pull request #1141 from friday/1137
Improve captions positioning consistency
2018-08-01 00:41:48 +10:00
Sam Potts 748aa5179f Comments about keydown vs keyup for Firefox 2018-08-01 00:38:19 +10:00
Sam Potts 56a485bac6 Fix Firefox spacebar issue 2018-08-01 00:37:55 +10:00
Albin Larsson 9488de30e5 Fix #1137: Improve captions positioning consistency 2018-07-31 16:26:34 +02:00
Sam Potts e3dfd16096 Merge pull request #1139 from friday/controls-input
Controls input fixes
2018-07-31 09:08:08 +10:00
Albin Larsson c230ccce86 Update controls.md docs 2018-07-31 00:44:07 +02:00
Albin Larsson db22a8e9c4 Improve handling of the 'controls' argument 2018-07-31 00:43:56 +02:00
Sam Potts 3a3358e2b4 Make iOS range fix more universal 2018-07-30 23:29:14 +10:00
Sam Potts 248005e8e0 Fix merge 2018-07-30 23:29:02 +10:00
Sam Potts dae272ef66 Merge branch 'develop' into a11y-improvements
# Conflicts:
#	demo/dist/demo.css
#	dist/plyr.css
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	package.json
#	src/js/plyr.js
2018-07-30 23:09:12 +10:00
Sam Potts 2679c5898e v3.2.23 2018-07-30 22:55:34 +10:00
Sam Potts efb7401e6d Merge pull request #1136 from sampotts/develop
v3.3.23
2018-07-30 22:54:03 +10:00
Sam Potts 60a0f0c979 Missed reference 2018-07-30 22:53:42 +10:00
Sam Potts 5d168d0e14 v3.3.23 2018-07-30 22:53:09 +10:00
Sam Potts f964e34d8c Merge pull request #1134 from mjfwebb/hide-empty-controls
Hide empty controls
2018-07-30 22:47:10 +10:00
Sam Potts 021ba0b8e9 Update controls.scss 2018-07-30 22:46:48 +10:00
Sam Potts 96d371546c Merge pull request #1135 from sampotts/master
Merge back
2018-07-30 22:45:59 +10:00
Sam Potts e1780a4df0 Fix for redraw issue 2018-07-30 22:44:19 +10:00
Albin Larsson e5e169a1e2 Don't move caption up when "showing" the lower controls when the controls are empty 2018-07-30 01:02:13 +02:00
mjfwebb 5eda498516 If the plyr__controls is empty it is still showing the transition causing captions to be pushed up when hovering over where the controls would be. This change hides the plyr__controls div when it is empty. 2018-07-29 22:02:16 +02:00
Sam Potts 599b33e55f Click to play fix, poster fix, iOS controls fixes 2018-07-30 01:13:12 +10:00
Sam Potts 3a8332bdb3 Fix for webkit redrawing issue 2018-07-29 12:32:26 +10:00
Sam Potts 44b5d9f6b9 Merge pull request #1131 from friday/1108
Make sure youtube.onReady doesn't run twice
2018-07-29 00:19:37 +10:00
Albin Larsson 24deff0e2d Fix #1108: Make sure youtube.onReady doesn't run twice 2018-07-28 04:04:57 +02:00
Sam Potts 0933b48c2a Merge pull request #1113 from friday/issue-templates
Use GitHub's new issue template prompt
2018-07-27 13:12:49 +10:00
Sam Potts 71578e07ec Merge pull request #1120 from didacte/add-missing-youtube-hl-param
Add support for YouTube's hl param
2018-07-26 00:03:10 +10:00
Léo Renaud-Allaire 671325dd17 Add support for YouTube's hl param 2018-07-24 16:36:25 -04:00
Sam Potts 53a3d06103 Merge branch 'develop' into a11y-improvements
# Conflicts:
#	demo/dist/demo.css
#	demo/dist/demo.js.map
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.css
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	package.json
#	yarn.lock
2018-07-24 09:38:26 +10:00
Albin Larsson ad1989e45e New issue templates (with prompt) 2018-07-23 06:21:46 +02:00
Albin Larsson f9ac98bc6d Lowercase contributing.md (like the other docs) 2018-07-23 06:21:25 +02:00
Sam Potts 544ab0086b Logic fix 2018-07-19 10:02:41 +10:00
Sam Potts 13bf80d372 Deployment improvements (auto purge cache etc) 2018-07-19 09:47:11 +10:00
Sam Potts e2fb922d73 v3.3.22 2018-07-18 21:47:46 +10:00
Sam Potts a6cc85c437 Merge branch 'develop' 2018-07-18 21:46:43 +10:00
Sam Potts d061be5d2b Changelog 2018-07-18 21:46:23 +10:00
Sam Potts dc2feedd79 Merge pull request #1103 from sampotts/develop
v3.3.12
2018-07-18 21:45:31 +10:00
Sam Potts f8e4ba36e5 Merge branch 'master' into develop
# Conflicts:
#	changelog.md
#	dist/plyr.js.map
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js.map
#	package.json
2018-07-18 21:44:39 +10:00
Sam Potts f3d5389587 3.3.17 2018-07-18 21:41:46 +10:00
Sam Potts d9ffb10b93 Merge pull request #1080 from gurupras/html5-quality-source-setter-readme
Updated README.md to show how to add quality options to HTML5 videos
2018-07-18 15:32:46 +10:00
Sam Potts e63ad7c74b Keyboard and focus improvements 2018-07-15 19:23:28 +10:00
Sam Potts 1186377b25 Merge pull request #1095 from friday/stickler
Add stickler config
2018-07-11 23:32:49 +10:00
Sam Potts 8616895e57 Merge pull request #1094 from friday/remark
Verify internal documentation links with "remark"
2018-07-11 09:17:24 +10:00
Albin Larsson 2cf5a22c85 Fix internal link for the source setter 2018-07-10 23:56:00 +02:00
Albin Larsson 763eb2df80 Add 'remark' and plugin to verify internal links in markdown 2018-07-10 23:56:00 +02:00
Albin Larsson 8bbf66a0fb Add stickler config 2018-07-10 20:53:09 +02:00
Sam Potts 676b46e4a7 Merge pull request #1093 from friday/travis-2
Verify PR instructions with Travis
2018-07-10 15:24:22 +10:00
Albin Larsson 82a119c67f Add travis check for the base branch (only permit develop for code changes) 2018-07-10 05:16:05 +02:00
Albin Larsson 6fd4389887 Add travis check for omitting dist in development branch 2018-07-10 05:16:05 +02:00
Albin Larsson 1e1a548459 Simplify travis conf 2018-07-10 02:44:15 +02:00
Sam Potts 8db9b53a8f Merge pull request #1092 from friday/readme-streaming-link
Fix streaming link in docs
2018-07-10 09:50:43 +10:00
Sam Potts ba33fd8277 Merge pull request #1091 from friday/1085
Add navigator.languages fallback for ios 9
2018-07-10 09:50:24 +10:00
Albin Larsson 8071feda18 Fix streaming link 2018-07-09 18:47:16 +02:00
Albin Larsson a49b73cd01 Add navigator.languages fallback for ios 9 2018-07-09 18:38:23 +02:00
Sam Potts 8226493a9e Update pull_request_template.md 2018-07-05 09:49:25 +10:00
Sam Potts 38a8a0e8a1 Update issue_template.md 2018-07-05 09:45:17 +10:00
Sam Potts ead6601394 Merge 2018-07-02 23:11:59 +10:00
Sam Potts e61ebd8d05 Merge branch 'develop' into a11y-improvements 2018-07-02 23:11:50 +10:00
Sam Potts 6eeca8b5d1 Merge pull request #1083 from friday/fix-travis
Change "no-cycle" lint-error to warning
2018-07-02 08:53:50 +10:00
Sam Potts bf51ce4414 Merge pull request #1082 from friday/yarn-error.log
Gitignore yarn-error.log
2018-07-02 08:53:32 +10:00
Albin Larsson 5ad614e251 Change linting import/no-cycle to warning (not error) 2018-07-01 19:50:58 +02:00
Albin Larsson 93c890603d Gitignore yarn-error.log 2018-07-01 19:33:47 +02:00
Guru Prasad Srinivasa 29fb4dfc2b Updated README.md to show up to add quality options to HTML5 videos
initialized via the source setter
2018-06-30 08:04:32 -04:00
Sam Potts 7de9fd1d65 Merge branch 'develop'
# Conflicts:
#	changelog.md
#	demo/dist/demo.css
#	demo/dist/demo.js.map
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.css
#	dist/plyr.js.map
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	package.json
#	readme.md
#	src/js/plyr.js
#	src/js/plyr.polyfilled.js
2018-06-29 00:43:02 +10:00
Sam Potts 566c059832 3.3.16 2018-06-29 00:38:11 +10:00
Sam Potts 0c9572f0a1 Merge branch 'develop' of github.com:sampotts/plyr into develop 2018-06-29 00:21:28 +10:00
Sam Potts c99607c85a Linting, housekeeping, duration fix (fixes #1074) 2018-06-29 00:21:22 +10:00
Sam Potts e2010bcd1a Merge pull request #1075 from mimse/feature/handle_live_stream
Hide currentTime and progress
2018-06-28 23:52:58 +10:00
Sam Potts 3bf1c59bd6 Work on key bindings for menu 2018-06-28 23:44:07 +10:00
mimse e9f1b55f51 Hide currentTime and progress 2018-06-28 11:25:55 +02:00
Sam Potts 4f5152f526 Merge pull request #1070 from mimse/fix_condition_check
Fixed condition check
2018-06-27 22:47:42 +10:00
Morten Vestergaard Hansen de9b53045a Fixed condition check
If class includes "control" it will add it again.
2018-06-27 14:33:51 +02:00
Sam Potts e59fe1aacf Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	src/js/listeners.js
2018-06-25 23:09:13 +10:00
Sam Potts df458c5e7a Merge branch 'develop' of github.com:sampotts/plyr into develop 2018-06-25 22:31:43 +10:00
Sam Potts e206554146 Linting and package tweak 2018-06-25 22:31:38 +10:00
Sam Potts e422806c44 Merge pull request #1063 from klassicd/develop
Handle undefined this.player.elements.buttons.play
2018-06-25 20:21:49 +10:00
Michael DePetrillo b6ddf144f4 handle undefined player.elements.buttons.play 2018-06-25 12:00:02 +02:00
Sam Potts 86406ee59a Merge pull request #1061 from friday/captions-no-toggle-button
Fix captions.toggle() if there is no toggle button
2018-06-22 08:19:33 +10:00
Albin Larsson 81c5477f1d Fix captions.toggle() if there is no toggle button 2018-06-21 15:22:30 +02:00
Sam Potts ac64350a5f Fix for bug where controls wouldn't show on hover over YouTube video 2018-06-21 19:26:56 +10:00
Sam Potts 333619f1e3 Remove pointer-events: none on embed <iframe> to comply with YouTube ToS 2018-06-21 14:26:40 +10:00
Sam Potts 17dcb63c26 Merge branch 'develop'
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
2018-06-21 09:11:05 +10:00
Sam Potts e04b90c9c0 Ads only on HTML5 and .is cleanup 2018-06-21 09:06:28 +10:00
Sam Potts 1f1d74ba50 Work on menus 2018-06-21 09:01:16 +10:00
Sam Potts f62e1da01a Merge pull request #1057 from meyt/patch-1
Fix i18n defaults path on README
2018-06-20 22:46:50 +10:00
Meyti 2fe949629f Fix i18n defaults path 2018-06-20 16:00:51 +04:30
Sam Potts 20f2ddc11d Merge pull request #1056 from friday/volume
Minor increaseVolume and decreaseVolume changes
2018-06-20 16:53:43 +10:00
Albin Larsson 004528a65c Avoid conditions in volume scroll event listener 2018-06-19 17:22:12 +02:00
Albin Larsson 39c7bd40c2 Make decreaseVolume wrap increaseVolume for code reuse 2018-06-19 16:29:52 +02:00
Albin Larsson 43879e08f4 Make (increase/decrease)Volume methods ignore invalid input instead of raising / lowering to the min / max 2018-06-19 16:27:06 +02:00
Sam Potts bb546fe43f Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
2018-06-19 19:24:47 +10:00
Sam Potts 5ed7aa6620 v3.3.17 2018-06-19 16:58:38 +10:00
Sam Potts 47750b6aad v3.3.17
-   Fix YouTube muting after seeking with the progress slider
-   Respect preload="none" when setting quality if the media hasn't been loaded some other way
2018-06-19 16:57:32 +10:00
Sam Potts de7832eb8b Merge branch 'develop' 2018-06-19 16:40:54 +10:00
friday 52ea5bd0ab Merge pull request #1052 from friday/youtube-audio-fix
Fix YouTube muting after seeking with the progress slider.
2018-06-19 04:15:07 +02:00
Albin Larsson 457d112df7 Fix #1045: YouTube mutes when seeking after play 2018-06-19 04:03:59 +02:00
Sam Potts 22cdec9d38 Merge pull request #1051 from friday/quality-2
Respect preload="none" when setting quality if the media hasn't been loaded some other way
2018-06-19 11:48:44 +10:00
Albin Larsson d72e502107 Fixes #1044: Don't load the new source if preload is disabled and the current source hasn't been loaded 2018-06-19 03:39:18 +02:00
Albin Larsson 94055f0772 Replace filter()[0] with find() 2018-06-19 03:35:57 +02:00
Sam Potts ede9323524 v3.3.16 2018-06-19 09:16:14 +10:00
Sam Potts c45f428f61 Built files 2018-06-19 09:14:10 +10:00
Sam Potts b61ba02f3d Fix issue with play button not changing state (fixes #1048) 2018-06-19 09:12:21 +10:00
Sam Potts 9e1218547b WIP 2018-06-19 09:11:35 +10:00
Sam Potts 715b88c09b Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
2018-06-18 23:29:25 +10:00
Sam Potts ea4d91d2a0 v3.3.15 2018-06-18 23:21:03 +10:00
Sam Potts 7b9ef7d757 More work on menus 2018-06-18 23:13:40 +10:00
Sam Potts d64ed4ba5a Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
2018-06-18 22:17:34 +10:00
Sam Potts 22d524ac9d Removed 1440p so I can afford to eat 2018-06-18 22:16:12 +10:00
Sam Potts 8584f6a1db v3.3.14 2018-06-18 22:01:56 +10:00
Sam Potts 08df96a149 v3.3.13
You guessed it, a load of awesome changes from contributors:

Thanks @friday for the following:

-   Captions fixes
-   Fix poster race conditions
-   Minor code improvements for quality switching
-   Minor event changes
-   Fix condition in events.toggleListener to allow non-elements
-   Suggestion: Remove array newline rule
-   Contributions improvements

-   fix: html5.cancelRequest not remove source tag correctly (thanks @a60814billy)
-   remove event listeners in destroy() (thanks @cky917)
-   Fix markdown in README (thanks @azu)
-   Some parts of the accessibility improvements outlined in #905 (more on the way...)
-   Fix for bug where volume slider didn't always show
2018-06-18 21:49:06 +10:00
Sam Potts cc3c0b5448 Merge branch 'develop' 2018-06-18 21:41:25 +10:00
Sam Potts ffd864ed39 Work on controls 2018-06-18 21:39:47 +10:00
Sam Potts 3c9c1b4cdc Merge pull request #1041 from sampotts/a11y-improvements
A11y improvements
2018-06-17 01:34:11 +10:00
Sam Potts 599883e684 Formatting fix 2018-06-17 01:30:24 +10:00
Sam Potts f1b4db4f36 Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	src/js/controls.js
#	src/js/fullscreen.js
#	src/js/plyr.js
#	src/js/ui.js
#	src/js/utils.js
2018-06-17 01:26:24 +10:00
Sam Potts d4abb4b143 120 line width, package upgrade 2018-06-17 01:04:55 +10:00
Sam Potts 828ce66942 Merge pull request #1038 from friday/captions-input-lowercase
Small captions fixes
2018-06-17 00:40:56 +10:00
Sam Potts ccc2608cf6 Merge pull request #1039 from friday/poster-race-conditions
Fix poster race conditions
2018-06-17 00:40:28 +10:00
Sam Potts de45de0e0b Merge pull request #1040 from friday/switches-get-stitches
Switches code optimizations
2018-06-17 00:39:35 +10:00
Albin Larsson 99c10aa1fc Replace switch in controls.createProgress with object literal 2018-06-16 07:27:04 +02:00
Albin Larsson 2a186e425b Replace switch in support.mime with object literal and conditions, and make it return boolean 2018-06-16 07:27:04 +02:00
Albin Larsson 64bb206d85 Replace switch in support.check with simpler conditions 2018-06-16 07:27:04 +02:00
Albin Larsson 2d6732d580 Replace switch in controls.createLabel with object literal 2018-06-16 07:25:34 +02:00
Albin Larsson 8f359adf9c Fix captions.toggle order 2018-06-16 01:34:55 +02:00
Albin Larsson 1f09493ba2 Captions: Handle uppercase input (like before) 2018-06-16 01:07:16 +02:00
Albin Larsson 115f352ade Respect call order and prioritize public API calls for setting poster, in order to avoid race conditions 2018-06-15 23:56:47 +02:00
Albin Larsson 2af60c5c0d Add 'ready' promise 2018-06-15 23:01:33 +02:00
Albin Larsson aab2817ddc Copy poster when creating new media element for YouTube and Vimeo (needed for #1018) 2018-06-15 22:57:16 +02:00
Albin Larsson f1c4752036 Filter out null / undefined in elements.setAttributes 2018-06-15 22:52:19 +02:00
Albin Larsson 88735e3146 Replace switch in controls.updateSetting with condition 2018-06-15 15:57:10 +02:00
Albin Larsson c373ed72d7 Replace switch in YouTube error handler with object literal 2018-06-15 15:57:10 +02:00
Albin Larsson 213cfe8c84 Replace switch in media.js with simpler conditions 2018-06-15 15:57:10 +02:00
Albin Larsson 87ea5e14b4 Replace provider switch plyr.js with conditions 2018-06-15 15:57:10 +02:00
Albin Larsson 2aa967aba9 Replace switch in source.js with destructuring 2018-06-15 12:33:30 +02:00
Sam Potts d522e40594 Merge pull request #1034 from friday/remove-array-newline-rule
Suggestion: Remove array newline rule
2018-06-15 15:34:29 +10:00
Sam Potts 3cd2b9a6c3 Merge pull request #1036 from friday/captions-passive-toggle
Captions fixes (again)
2018-06-15 15:33:39 +10:00
Albin Larsson 19e412a73a Add 'passive' flag to internal captions methods to avoid overriding user preferences, support multiple browser languages (get first match) and improve comments 2018-06-15 06:07:04 +02:00
Albin Larsson cf5f77c709 Fix menu transitionend event listener 2018-06-15 05:51:23 +02:00
Sam Potts 4811e3333f Merge pull request #1035 from sampotts/friday-contrib-2
Contributions improvements
2018-06-15 11:04:33 +10:00
Albin Larsson 8257857075 Wrap caption toggle event listener callback to avoid sending event 2018-06-15 02:44:00 +02:00
friday e3e4e60fdb Contributions improvements
General improvements and new sections
2018-06-14 19:50:59 +02:00
Albin Larsson 6ce9a94932 Move internal event listeners for captions with direct handling in the captions object 2018-06-14 16:41:16 +02:00
Albin Larsson fa5d0ad316 Move toggleCaption internals to captions object 2018-06-14 15:58:35 +02:00
Albin Larsson 6bff6b317d Remove line breaks in arrays 2018-06-13 23:27:35 +02:00
Albin Larsson 99ac8d4c52 Remove array-newline rule 2018-06-13 22:07:32 +02:00
Sam Potts 019e1f80ca Merge pull request #1032 from friday/event-2
Fix condition in events.toggleListener to allow non-elements
2018-06-13 23:16:46 +10:00
Albin Larsson 2fe98f3721 Fix condition in events.toggleListener to allow non-elements 2018-06-13 14:29:55 +02:00
Sam Potts 5c08363400 Merge pull request #1030 from friday/event-improvements
Minor event changes
2018-06-13 10:52:17 +10:00
Albin Larsson 927326f715 Also remove 'once' event listeners when destroying (they may still be waiting) 2018-06-12 20:00:41 +02:00
Albin Larsson 53933dff7e Use toggleListener in trapFocus 2018-06-12 19:39:26 +02:00
Albin Larsson f15c1344b0 Removed support for multiple elements in toggleListener 2018-06-12 19:10:00 +02:00
Albin Larsson fb48b330cc typo 2018-06-12 17:41:17 +02:00
Sam Potts 5dddf8b0ec Logic cleanup 2018-06-13 00:56:31 +10:00
Sam Potts 0ecf7e3854 Force string on format 2018-06-13 00:48:42 +10:00
Sam Potts aae1092bac Merge branch 'develop' of github.com:sampotts/plyr into develop
# Conflicts:
#	src/js/captions.js
#	src/js/controls.js
#	src/js/fullscreen.js
#	src/js/html5.js
#	src/js/listeners.js
#	src/js/plugins/youtube.js
#	src/js/plyr.js
#	src/js/utils.js
2018-06-13 00:41:30 +10:00
Sam Potts 7158e507ad Merge pull request #1029 from cky917/develop
remove event listeners in destroy()
2018-06-13 00:05:31 +10:00
Sam Potts 70f3390ffe Merge pull request #1028 from a60814billy/fix/cancel-request-not-remove-source-tag-correctly
fix: html5.cancelRequest not remove source tag correctly
2018-06-13 00:03:24 +10:00
Sam Potts 392dfd024c Utils broken down into seperate files and exports 2018-06-13 00:02:55 +10:00
cky 87170ab460 remove event listeners in destroy, add once method 2018-06-12 21:18:05 +08:00
BoHong Li ee4c044d27 fix: html5.cancelRequest not remove source tag correctly 2018-06-12 11:35:31 +08:00
Sam Potts 0b09b8ee6f Merge pull request #1027 from friday/quality
Minor code improvements for quality switching
2018-06-12 11:13:34 +10:00
Albin Larsson db95b3234f Move uniqueness filter from getQualityOptions to setQualityMenu 2018-06-12 02:31:18 +02:00
Albin Larsson 6d2dad5810 Trigger qualityrequested event unconditionally when trying to set it (needed for streaming libraries to be able to listen) 2018-06-12 02:31:18 +02:00
Albin Larsson 81ee3f759c Remove todo comment about Vimeo support for setting quality (they don't support it) 2018-06-12 02:31:18 +02:00
Albin Larsson ed606c28ab Filter out unsupported mimetypes in getSources() instead of the quality setter 2018-06-12 02:31:18 +02:00
Albin Larsson f15e07f7f5 Simplify logic in youtube.mapQualityUnit (not that it matters much now) 2018-06-12 02:31:04 +02:00
Sam Potts cd14c3086d Merge pull request #1025 from azu/patch-1
Fix markdown in README
2018-06-11 20:16:18 +10:00
azu 41184b82ee Fix markdown in README 2018-06-11 19:12:35 +09:00
Sam Potts 6a6f3914c0 Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
2018-06-11 17:13:09 +10:00
Sam Potts 1bc452c349 Merge 2018-06-11 16:54:35 +10:00
Sam Potts 3fad6ed42c Merge branch 'develop' into a11y-improvements
# Conflicts:
#	demo/dist/demo.css
#	dist/plyr.css
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	src/js/captions.js
2018-06-11 16:54:20 +10:00
Albin Larsson 62c263bda3 Replace quality setter conditions with Array.find() 2018-06-11 08:23:08 +02:00
Albin Larsson 4c1337b4c5 Assure type safety in getSources() and getQualityOptions() (always return arrays), and remove external conditions and type conversion no longer needed 2018-06-11 08:23:08 +02:00
Sam Potts 38f10d4cc6 WIP 2018-06-11 16:19:11 +10:00
Sam Potts 7c6d4666e9 Merge branch 'develop' into a11y-improvements
# Conflicts:
#	demo/dist/demo.css
#	dist/plyr.css
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	src/js/captions.js
#	src/js/plyr.js
2018-06-09 17:03:16 +10:00
Sam Potts 90c5735904 WIP 2018-05-28 10:19:07 +10:00
113 changed files with 28153 additions and 29520 deletions
+8 -15
View File
@@ -5,8 +5,12 @@
"browser": true,
"es6": true
},
"globals": { "Plyr": false, "jQuery": false },
"globals": {
"Plyr": false,
"jQuery": false
},
"rules": {
"import/no-cycle": 1,
"no-const-assign": 1,
"no-shadow": 0,
"no-this-before-super": 1,
@@ -21,20 +25,9 @@
"eqeqeq": [2, "always"],
"one-var": [2, "never"],
"comma-dangle": [2, "always-multiline"],
"no-restricted-globals": [
"error",
{
"name": "event",
"message": "Use local parameter instead."
},
{
"name": "error",
"message": "Use local parameter instead."
}
],
"no-param-reassign": [2, { "props": false }],
"array-bracket-newline": [2, { "minItems": 2 }],
"array-element-newline": [2, { "minItems": 2 }]
"spaced-comment": [2, "always"],
"no-restricted-globals": 2,
"no-param-reassign": [2, { "props": false }]
},
"parserOptions": {
"sourceType": "module"
+55
View File
@@ -0,0 +1,55 @@
---
name: Bug report
about: Report an issue or unexpected behaviour with Plyr
---
<!--
Before creating the issue, please make sure that...
* You aren't getting any errors in your own code, causing the problem.
* You are using the latest version of Plyr.
* There isn't already an open issue for your problem.
* You are following the documentation correctly (https://github.com/sampotts/plyr/)
* Your problem doesn't happen if you remove Plyr and use native HTML5 media (when applicable).
For problems with autoplay, see our FAQ (https://github.com/sampotts/plyr/wiki/FAQ)
If you have multiple unrelated problems, create separate issues rather than combining them into one.
Note that leaving sections blank or being vague will make it difficult for us to troubleshoot and we may close the issue.
-->
### Expected behaviour
### Actual behaviour
### Steps to reproduce
### Environment
- Browser:
- Version:
- Operating System:
- Version:
### Console errors (if any)
### Link to where the bug is happening
<!--
This link can be either to our demo at https://plyr.io/ if the problem can be observed there, or to a code playground with a **minimal** test case that demonstrates the problem.
You can use one of our prepared templates to get started creating the test case:
* HTML5 video: https://codepen.io/pen?template=bKeqpr
* HTML5 audio: https://codepen.io/pen?template=rKLywR
* YouTube: https://codepen.io/pen?template=GGqbbJ
* Vimeo: https://codepen.io/pen?template=bKeXNq
* Dash.js integration: https://codepen.io/pen?template=zaBgBy
* Hls.js integration: https://codepen.io/pen?template=oyLKQb
* Shaka Player integration: https://codepen.io/pen?template=ZRpzZO
It's important that you keep the issue description and replication demo **minimal**. If your replication includes frameworks, libraries or customizations, this makes it much harder to understand the problem and find the bug. For more help on how to create the demo, see https://github.com/sampotts/plyr/wiki/Writing-helpful-issue-descriptions
-->
+10
View File
@@ -0,0 +1,10 @@
---
name: New feature
about: Request new functionality
---
<!--
Please describe the behaviour that you want to add, and why. Be as clear as possible to avoid confusion.
If you want to request multiple features that aren't directly related, then create one issue per feature.
-->
+10
View File
@@ -0,0 +1,10 @@
---
name: Improvement
about: Request a change that isn't a bug or new feature
---
<!--
Please describe the behaviour that you want to change, and why. Be as clear as possible to avoid confusion.
If you want to request multiple changes that aren't directly related, then create one issue per change.
-->
+2 -16
View File
@@ -1,17 +1,3 @@
<!---
Please use this issue template as it makes replicating and fixing the issue easier!
--->
PLEASE USE OUR SPECIFIC ISSUE TEMPLATES for bug reports, features and improvement suggestions.
### Expected behaviour
### Actual behaviour
### Environment
- Browser:
- Version:
- Operating System:
- Version:
### Steps to reproduce
-
Our issue tracker is not for support questions. If you need help, follow our support instructions: https://github.com/sampotts/plyr/blob/master/contributing.md#support
+1 -1
View File
@@ -4,5 +4,5 @@
### Checklist
- [ ] Use `develop` as the base branch
- [ ] Exclude the gulp build from the PR
- [ ] Exclude the gulp build (`/dist` changes) from the PR
- [ ] Test on [supported browsers](https://github.com/sampotts/plyr#browser-support)
+3 -3
View File
@@ -1,11 +1,11 @@
node_modules
.DS_Store
aws.json
credentials.json
*.mp4
!dist/blank.mp4
index-*.html
npm-debug.log
yarn-error.log
package-lock.json
*.webm
/package-lock.json
.idea/
+7
View File
@@ -2,3 +2,10 @@ demo
.github
.vscode
*.code-workspace
credentials.json
bundles.json
yarn.lock
package-lock.json
*.mp4
*.webm
!dist/blank.mp4
+2 -2
View File
@@ -1,7 +1,7 @@
{
"useTabs": false,
"tabWidth": 4,
"printWidth": 160,
"singleQuote": true,
"trailingComma": "all"
"trailingComma": "all",
"printWidth": 120
}
+5
View File
@@ -0,0 +1,5 @@
linters:
eslint:
files:
ignore:
- 'node_modules/*'
+5 -4
View File
@@ -1,7 +1,8 @@
language: node_js
node_js:
- 'lts/*'
node_js: lts/*
script:
- npm run lint
- npm run build
- bash .travis/prevent-base-master.sh
- bash .travis/omit-dist.sh
- npm run lint
- npm run build
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
if [ $TRAVIS_BRANCH == "develop" ] && $(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qE "^(demo/)?dist/"); then
echo 'Build output ("dist" and "demo/dist") not permitted in develop' >&2
exit 1
fi
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
if [ "$TRAVIS_PULL_REQUEST" != "false" ] && [ $TRAVIS_BRANCH == "master" ] && $(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -q "^src/"); then
echo 'The base branch for pull requests must be "develop"' >&2
exit 1
fi
-52
View File
@@ -1,52 +0,0 @@
# Contributing
We welcome bug reports, feature requests and pull requests. If you want to help us out, please follow these guidelines, in order to avoid redundant work.
## Reporting issues
Our GitHub issue tracker is for bug reports and feature requests. Don't create support issues here. Use [Stack Overflow](https://stackoverflow.com/) or [our Slack](https://bit.ly/plyr-chat) for that.
Please verify that your issue hasn't already been answered by our FAQ (https://github.com/sampotts/plyr/wiki/FAQ), or that there isn't already an open issue for it.
When applicable, check that your problem doesn't happen without Plyr (see [FAQ#1](https://github.com/sampotts/plyr/wiki/FAQ#1-does-plyr-work-with--)).
Verify that you are following the documentation, are using the latest version of Plyr, and aren't getting any errors in your own code, causing the issues.
Describe the issue as detailed as possible, answering these questions:
* Does it happen only with specific options and/or specific browsers?
* Does is happen only with HTML5 video, audio, youtube, vimeo or a specific library?
* Does the issue happen on [our demo](https://plyr.io/)? If not, please recreate it with a **minimal** example online. You can use our Codepen templates to get started:
* [HTML5 video](https://codepen.io/pen?template=bKeqpr)
* [HTML5 audio](https://codepen.io/pen?template=rKLywR)
* [YouTube](https://codepen.io/pen?template=GGqbbJ)
* [Vimeo](https://codepen.io/pen?template=bKeXNq)
* [Dash.js integration](https://codepen.io/pen?template=zaBgBy)
* [Hls.js integration](https://codepen.io/pen?template=oyLKQb)
* [Shaka Player integration](https://codepen.io/pen?template=ZRpzZO)
It's important that you keep the issue description and replication demo **minimal**. If your implementation is using a framework, library or custom methods, which aren't needed to reproduce the issue, this makes it harder to debug and understand the issue. While it may be relevant to bring this up (ex: "I need Plyr to trigger the event sooner or it breaks Framework X") it also means that the person who is trying to fix the issue either has to know or learn your frameworks, libraries and custom methods, or that no one will try to fix your issue because it's too much work.
In order to keep things on topic and to avoid bothering people with github notifications, please don't combine multiple problems or bugs into one issue, don't comment on issues unless your comment is related to that issue, and don't post "+1" or "I agree" type of comments. Use the emojis instead.
Last but not least: Keep a civil tone in issues and comments. Non-constructive comments may be removed.
## Requesting features and improvements
If you are missing something in Plyr, you can create a GitHub issue for this as well. Since we prioritize fixing bugs first, and may have a lot of other suggestions and architectural changes to work on as well, these may not be at the top of our list. If it's important or urgent to you, you may want to first ensure it's something we want to have in Plyr, and then contribute it as a pull request.
## Contributing features and documentation
* Fork Plyr, and create a new branch in your fork, based on the **develop** branch
* To test locally, you can use the demo. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build while you are working, and run a local server from the repository root directory. If you have Python installed, this command should work: `python -m SimpleHTTPServer 8080`. Then go to `http://localhost:8080/demo/`
* Develop and test your modifications.
* Preferably commit your changes as independent logical chunks, with meaningful messages. Make sure you do not commit unnecessary files or changes, such as logging or breakpoints you added for testing, and the build output.
* If your modifications changes the documented behavior or add new features, document these changes in readme.md.
* When finished, push the changes to your GitHub repository and send a pull request to **develop**. Describe what your PR does.
* If the Travis build fails, or if you get a code review with change requests, you can fix these by pushing new or rebased commits to the branch.
-33
View File
@@ -1,33 +0,0 @@
{
"name": "plyr",
"description": "A simple HTML5 media player using custom controls",
"homepage": "http://plyr.io",
"keywords": [
"Audio",
"Video",
"HTML5 Audio",
"HTML5 Video"
],
"authors": [
"Sam Potts <sam@potts.es>"
],
"dependencies": {},
"main": [
"dist/plyr.css",
"dist/plyr.js",
"dist/plyr.svg",
"src/less/plyr.less",
"src/scss/plyr.scss",
"src/js/plyr.js"
],
"ignore": [
"node_modules",
"bower_components",
".gitignore"
],
"repository": {
"type": "git",
"url": "git://github.com/sampotts/plyr.git"
},
"license": "MIT"
}
+277 -164
View File
File diff suppressed because it is too large Load Diff
+40
View File
@@ -0,0 +1,40 @@
# Contributing
We welcome bug reports, feature requests and pull requests. If you want to help us out, please follow these guidelines, in order to avoid redundant work.
## Support
Before asking questions, read our [documentation](https://github.com/sampotts/plyr) and [FAQ](https://github.com/sampotts/plyr/wiki/FAQ).
If these doesn't answer your question
* Use [Stack Overflow](https://stackoverflow.com/) for questions that doesn't directly involve Plyr. This includes for example how to use Javascript, CSS or HTML5 media in general, and how to use other frameworks, libraries and technology.
* Use [our Slack](https://bit.ly/plyr-chat) if you need help using Plyr or have questions about Plyr.
## Commenting
When commenting, keep a civil tone and stay on topic. Don't ask for [support](#support), or post "+1" or "I agree" type of comments. Use the emojis instead.
Asking for the status on issues is discouraged. Unless someone has explicitly said in an issue that it's work in progress, most likely that means no one is working on it. We have a lot to do, and it may not be a top priority for us.
We *may* moderate discussions. We do this to avoid threads being "hijacked", to avoid confusion in case the content is misleading or outdated, and to avoid bothering people with github notifications.
## Creating issues
Please follow the instructions in our issue templates. Don't use github issues to ask for [support](#support).
## Contributing features and documentation
* If you want to add a feature or make critical changes, you may want to ensure that this is something we also want (so you don't waste your time). Ask us about this in the corresponding issue if there is one, or on [our Slack](https://bit.ly/plyr-chat) otherwise.
* Fork Plyr, and create a new branch in your fork, based on the **develop** branch
* To test locally, you can use the demo. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build while you are working, and run a local server from the repository root directory. If you have Python installed, this command should work: `python -m SimpleHTTPServer 8080`. Then go to `http://localhost:8080/demo/`
* Develop and test your modifications.
* Preferably commit your changes as independent logical chunks, with meaningful messages. Make sure you do not commit unnecessary files or changes, such as the build output, or logging and breakpoints you added for testing.
* If your modifications changes the documented behavior or add new features, document these changes in readme.md.
* When finished, push the changes to your GitHub repository and send a pull request to **develop**. Describe what your PR does.
* If the Travis build fails, or if you get a code review with change requests, you can fix these by pushing new or rebased commits to the branch.
+15 -12
View File
@@ -2,9 +2,11 @@
This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs. You can pass the following to the `controls` option:
* `Array` of options (this builds the default controls based on your choices)
* `String` containing the desired HTML
* `Function` that will be executed and should return one of the above
- `Array` of options (this builds the default controls based on your choices)
- `Element` with the controls
- `String` containing the desired HTML
- `false` (or empty string or array) to disable all controls
- `Function` that will be executed and should return one of the above
## Using default controls
@@ -26,6 +28,7 @@ controls: [
'settings', // Settings menu
'pip', // Picture-in-picture (currently Safari only)
'airplay', // Airplay (currently Safari only)
'download', // Show a download button with a link to either the current source or a custom URL you specify in your options
'fullscreen', // Toggle fullscreen
];
```
@@ -81,14 +84,14 @@ The classes and data attributes used in your template should match the `selector
You need to add several placeholders to your HTML template that are replaced when rendering:
* `{id}` - the dynamically generated ID for the player (for form controls)
* `{seektime}` - the seek time specified in options for fast forward and rewind
* `{title}` - the title of your media, if specified
- `{id}` - the dynamically generated ID for the player (for form controls)
- `{seektime}` - the seek time specified in options for fast forward and rewind
- `{title}` - the title of your media, if specified
### Limitations
* Currently the settings menus are not supported with custom controls HTML
* AirPlay and PiP buttons can be added but you will have to manage feature detection
- Currently the settings menus are not supported with custom controls HTML
- AirPlay and PiP buttons can be added but you will have to manage feature detection
### Example
@@ -105,7 +108,7 @@ const controls = `
<svg role="presentation"><use xlink:href="#plyr-rewind"></use></svg>
<span class="plyr__tooltip" role="tooltip">Rewind {seektime} secs</span>
</button>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Play, {title}" data-plyr="play">
<button type="button" class="plyr__control" aria-label="Play, {title}" data-plyr="play">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-pause"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-play"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Pause</span>
@@ -122,7 +125,7 @@ const controls = `
</div>
<div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
<div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute">
<button type="button" class="plyr__control" aria-label="Mute" data-plyr="mute">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Unmute</span>
@@ -131,13 +134,13 @@ const controls = `
<div class="plyr__volume">
<input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" aria-label="Volume">
</div>
<button type="button" class="plyr__control" aria-pressed="true" aria-label="Enable captions" data-plyr="captions">
<button type="button" class="plyr__control" data-plyr="captions">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-captions-off"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Disable captions</span>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enable captions</span>
</button>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Enter fullscreen" data-plyr="fullscreen">
<button type="button" class="plyr__control" data-plyr="fullscreen">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-exit-fullscreen"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-enter-fullscreen"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Exit fullscreen</span>
+1 -1
View File
File diff suppressed because one or more lines are too long
+4351 -4367
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
+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
+1 -1
View File
File diff suppressed because one or more lines are too long
+15 -14
View File
@@ -91,21 +91,22 @@
</header>
<main>
<video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
<!-- Video files -->
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4" type="video/mp4" size="1440">
<div id="container">
<video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
<!-- Video files -->
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
<!-- Caption files -->
<track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
default>
<track kind="captions" label="Français" srclang="fr" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
<!-- Caption files -->
<track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
default>
<track kind="captions" label="Français" srclang="fr" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
<!-- Fallback for browsers that don't support the <video> element -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
</video>
<!-- Fallback for browsers that don't support the <video> element -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
</video>
</div>
<ul>
<li class="plyr__cite plyr__cite--video" hidden>
@@ -166,7 +167,7 @@
</svg>
<p>If you think Plyr's good,
<a href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts"
target="_blank" data-shr-network="twitter">tweet it</a>
target="_blank" data-shr-network="twitter">tweet it</a> 👍
</p>
</aside>
+47 -72
View File
@@ -7,16 +7,17 @@
import Raven from 'raven-js';
(() => {
const isLive = window.location.host === 'plyr.io';
// Raven / Sentry
// For demo site (https://plyr.io) only
if (isLive) {
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
}
const { host } = window.location;
const env = {
prod: host === 'plyr.io',
dev: host === 'dev.plyr.io',
};
document.addEventListener('DOMContentLoaded', () => {
Raven.context(() => {
const selector = '#player';
const container = document.getElementById('container');
if (window.shr) {
window.shr.setup({
count: {
@@ -30,6 +31,10 @@ import Raven from 'raven-js';
// Remove class on blur
document.addEventListener('focusout', event => {
if (!event.target.classList || container.contains(event.target)) {
return;
}
event.target.classList.remove(tabClassName);
});
@@ -42,12 +47,18 @@ import Raven from 'raven-js';
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
document.activeElement.classList.add(tabClassName);
}, 0);
const focused = document.activeElement;
if (!focused || !focused.classList || container.contains(focused)) {
return;
}
focused.classList.add(tabClassName);
}, 10);
});
// Setup the player
const player = new Plyr('#player', {
const player = new Plyr(selector, {
debug: true,
title: 'View From A Blue Moon',
iconUrl: '../dist/plyr.svg',
@@ -57,56 +68,6 @@ import Raven from 'raven-js';
tooltips: {
controls: true,
},
/* controls: [
'play-large',
'restart',
'rewind',
'play',
'fast-forward',
'progress',
'current-time',
'duration',
'mute',
'volume',
'captions',
'settings',
'pip',
'airplay',
'fullscreen',
], */
/* i18n: {
restart: '重新開始',
rewind: '快退{seektime}秒',
play: '播放',
pause: '暫停',
fastForward: '快進{seektime}秒',
seek: '尋求',
played: '發揮',
buffered: '緩衝的',
currentTime: '當前時間戳',
duration: '長短',
volume: '音量',
mute: '靜音',
unmute: '取消靜音',
enableCaptions: '開啟字幕',
disableCaptions: '關閉字幕',
enterFullscreen: '進入全螢幕',
exitFullscreen: '退出全螢幕',
frameTitle: '球員為{title}',
captions: '字幕',
settings: '設定',
speed: '速度',
normal: '正常',
quality: '質量',
loop: '循環',
start: 'Start',
end: 'End',
all: 'All',
reset: '重啟',
disabled: '殘',
enabled: '啟用',
advertisement: '廣告',
}, */
captions: {
active: true,
},
@@ -114,7 +75,7 @@ import Raven from 'raven-js';
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: {
enabled: true,
enabled: env.prod || env.dev,
publisherId: '918848828995742',
},
});
@@ -143,7 +104,11 @@ import Raven from 'raven-js';
// 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)) {
if (
!(type in types) ||
(!init && type === currentType) ||
(!currentType.length && type === types.video)
) {
return;
}
@@ -215,10 +180,12 @@ import Raven from 'raven-js';
case types.youtube:
player.source = {
type: 'video',
sources: [{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
}],
sources: [
{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
},
],
};
break;
@@ -226,10 +193,12 @@ import Raven from 'raven-js';
case types.vimeo:
player.source = {
type: 'video',
sources: [{
src: 'https://vimeo.com/76979871',
provider: 'vimeo',
}],
sources: [
{
src: 'https://vimeo.com/76979871',
provider: 'vimeo',
},
],
};
break;
@@ -302,11 +271,17 @@ import Raven from 'raven-js';
});
});
// Raven / Sentry
// For demo site (https://plyr.io) only
if (env.prod) {
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
}
// Google analytics
// For demo site (https://plyr.io) only
/* eslint-disable */
if (isLive) {
(function(i, s, o, g, r, a, m) {
if (env.prod) {
((i, s, o, g, r, a, m) => {
i.GoogleAnalyticsObject = r;
i[r] =
i[r] ||
+6 -3
View File
@@ -7,7 +7,8 @@
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-light;
src: url('https://cdn.plyr.io/static/fonts/gordita-light.woff2') format('woff2'), url('https://cdn.plyr.io/static/fonts/gordita-light.woff') format('woff');
src: url('https://cdn.plyr.io/static/fonts/gordita-light.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-light.woff') format('woff');
}
@font-face {
@@ -33,7 +34,8 @@
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-bold;
src: url('https://cdn.plyr.io/static/fonts/gordita-bold.woff2') format('woff2'), url('https://cdn.plyr.io/static/fonts/gordita-bold.woff') format('woff');
src: url('https://cdn.plyr.io/static/fonts/gordita-bold.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-bold.woff') format('woff');
}
@font-face {
@@ -41,5 +43,6 @@
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-black;
src: url('https://cdn.plyr.io/static/fonts/gordita-black.woff2') format('woff2'), url('https://cdn.plyr.io/static/fonts/gordita-black.woff') format('woff');
src: url('https://cdn.plyr.io/static/fonts/gordita-black.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-black.woff') format('woff');
}
+3
View File
@@ -11,6 +11,9 @@ $plyr-font-size-small: 12px;
$plyr-font-size-time: 11px;
$plyr-font-size-badges: 9px;
// Other
$plyr-font-smoothing: true;
// Captions
$plyr-font-size-captions-base: $plyr-font-size-base;
$plyr-font-size-captions-small: $plyr-font-size-small;
+2 -1
View File
@@ -2,7 +2,8 @@
// Typography
// ==========================================================================
$font-sans-serif: 'Gordita', 'Avenir', 'Helvetica Neue', sans-serif;
$font-sans-serif: 'Gordita', 'Avenir', 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol';
$font-size-base: 15;
$font-size-small: 13;
+1 -1
View File
File diff suppressed because one or more lines are too long
+6676 -7154
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
+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
+9291 -12462
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
+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
+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

+134 -92
View File
@@ -12,7 +12,6 @@ const concat = require('gulp-concat');
const filter = require('gulp-filter');
const sass = require('gulp-sass');
const cleancss = require('gulp-clean-css');
const run = require('run-sequence');
const header = require('gulp-header');
const prefix = require('gulp-autoprefixer');
const gitbranch = require('git-branch');
@@ -29,18 +28,12 @@ const sourcemaps = require('gulp-sourcemaps');
const uglify = require('gulp-uglify-es').default;
const commonjs = require('rollup-plugin-commonjs');
const resolve = require('rollup-plugin-node-resolve');
const FastlyPurge = require('fastly-purge');
const through = require('through2');
const bundles = require('./bundles.json');
const pkg = require('./package.json');
// Get AWS config
let aws = {};
try {
aws = require('./aws.json'); //eslint-disable-line
} catch (e) {
// Do nothing
}
const minSuffix = '.min';
// Paths
@@ -50,7 +43,7 @@ const paths = {
// Source paths
src: {
sass: path.join(root, 'src/sass/**/*.scss'),
js: path.join(root, 'src/js/**/*'),
js: path.join(root, 'src/js/**/*.js'),
sprite: path.join(root, 'src/sprite/*.svg'),
},
@@ -61,7 +54,7 @@ const paths = {
// Source paths
src: {
sass: path.join(root, 'demo/src/sass/**/*.scss'),
js: path.join(root, 'demo/src/js/**/*'),
js: path.join(root, 'demo/src/js/**/*.js'),
},
// Output paths
@@ -94,33 +87,33 @@ const sizeOptions = { showFiles: true, gzip: true };
const browsers = ['> 1%'];
// Babel config
const babelrc = {
presets: [[
'env',
{
targets: {
browsers,
const babelrc = (polyfill = false) => ({
presets: [
[
'@babel/preset-env',
{
targets: {
browsers,
},
useBuiltIns: polyfill ? 'usage' : false,
modules: false,
},
useBuiltIns: true,
modules: false,
},
]],
plugins: ['external-helpers'],
],
],
babelrc: false,
exclude: 'node_modules/**',
};
});
// Clean out /dist
gulp.task('clean', () => {
const dirs = [
paths.plyr.output,
paths.demo.output,
].map(dir => path.join(dir, '**/*'));
gulp.task('clean', done => {
const dirs = [paths.plyr.output, paths.demo.output].map(dir => path.join(dir, '**/*'));
// Don't delete the mp4
dirs.push(`!${path.join(paths.plyr.output, '**/*.mp4')}`);
del(dirs);
done();
});
const build = {
@@ -129,6 +122,7 @@ const build = {
const name = `js:${key}`;
tasks.js.push(name);
const { output } = paths[bundle];
const polyfill = name.includes('polyfilled');
return gulp.task(name, () =>
gulp
@@ -138,11 +132,7 @@ const build = {
.pipe(
rollup(
{
plugins: [
resolve(),
commonjs(),
babel(babelrc),
],
plugins: [resolve(), commonjs(), babel(babelrc(polyfill))],
},
options,
),
@@ -187,9 +177,11 @@ const build = {
.src(paths[bundle].src.sprite)
.pipe(
svgmin({
plugins: [{
removeDesc: true,
}],
plugins: [
{
removeDesc: true,
},
],
}),
)
.pipe(svgstore())
@@ -210,37 +202,40 @@ build.sass(bundles.demo.sass, 'demo');
build.js(bundles.demo.js, 'demo', { format: 'iife' });
// Build all JS
gulp.task('js', () => {
run(tasks.js);
});
gulp.task('js', () => gulp.parallel(tasks.js));
// Watch for file changes
gulp.task('watch', () => {
// Plyr core
gulp.watch(paths.plyr.src.js, tasks.js);
gulp.watch(paths.plyr.src.sass, tasks.sass);
gulp.watch(paths.plyr.src.sprite, tasks.sprite);
gulp.watch(paths.plyr.src.js, gulp.parallel(tasks.js));
gulp.watch(paths.plyr.src.sass, gulp.parallel(tasks.sass));
gulp.watch(paths.plyr.src.sprite, gulp.parallel(tasks.sprite));
// Demo
gulp.watch(paths.demo.src.js, tasks.js);
gulp.watch(paths.demo.src.sass, tasks.sass);
gulp.watch(paths.demo.src.js, gulp.parallel(tasks.js));
gulp.watch(paths.demo.src.sass, gulp.parallel(tasks.sass));
});
// Build distribution
gulp.task('build', () => {
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite);
});
gulp.task('build', gulp.series(tasks.clean, gulp.parallel(tasks.js, tasks.sass, tasks.sprite)));
// Default gulp task
gulp.task('default', () => {
run('build', 'watch');
});
gulp.task('default', gulp.series('build', 'watch'));
// Publish a version to CDN and demo
// --------------------------------------------
// If aws is setup
if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
// Get deployment config
let credentials = {};
try {
credentials = require('./credentials.json'); //eslint-disable-line
} catch (e) {
// Do nothing
}
// If deployment is setup
if (Object.keys(credentials).includes('aws') && Object.keys(credentials).includes('fastly')) {
const { version } = pkg;
const { aws, fastly } = credentials;
// Get branch info
const branch = {
@@ -248,10 +243,6 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
master: 'master',
develop: 'develop',
};
const allowed = [
branch.master,
branch.develop,
];
const maxAge = 31536000; // 1 year
const options = {
@@ -279,33 +270,51 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
},
};
const regex = '(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*).(?:0|[1-9][0-9]*)(?:-[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?(?:\\+[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?';
const regex =
'(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*).(?:0|[1-9][0-9]*)(?:-[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?(?:\\+[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?';
const semver = new RegExp(`v${regex}`, 'gi');
const localPath = new RegExp('(../)?dist', 'gi');
const versionPath = `https://${aws.cdn.domain}/${version}`;
const cdnpath = new RegExp(`${aws.cdn.domain}/${regex}/`, 'gi');
const renameFile = rename(p => {
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
});
// Check we're on the correct branch to deploy
const canDeploy = () => {
const allowed = [branch.master, branch.develop];
if (!allowed.includes(branch.current)) {
console.error(`Must be on ${allowed.join(', ')} to publish! (current: ${branch.current})`);
return false;
}
return true;
};
gulp.task('version', () => {
if (!canDeploy()) {
return null;
}
console.log(`Updating versions to '${version}'...`);
// Replace versioned URLs in source
const files = [
'plyr.js',
'plyr.polyfilled.js',
'defaults.js',
];
const files = ['plyr.js', 'plyr.polyfilled.js', 'config/defaults.js'];
return gulp
.src(files.map(file => path.join(root, `src/js/${file}`)))
.src(files.map(file => path.join(root, `src/js/${file}`)), { base: '.' })
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest(path.join(root, 'src/js/')));
.pipe(gulp.dest('./'));
});
// Publish version to CDN bucket
gulp.task('cdn', () => {
if (!allowed.includes(branch.current)) {
console.error(`Must be on ${allowed.join(', ')} to publish! (current: ${branch.current})`);
if (!canDeploy()) {
return null;
}
@@ -315,14 +324,14 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
return (
gulp
.src(paths.upload)
.pipe(
rename(p => {
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
}),
)
.pipe(renameFile)
// Remove min suffix from source map URL
.pipe(replace(/sourceMappingURL=([\w-?.]+)/, (match, p1) => `sourceMappingURL=${p1.replace(minSuffix, '')}`))
.pipe(
replace(
/sourceMappingURL=([\w-?.]+)/,
(match, p1) => `sourceMappingURL=${p1.replace(minSuffix, '')}`,
),
)
.pipe(
size({
showFiles: true,
@@ -334,18 +343,46 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
);
});
// Purge the fastly cache incase any 403/404 are cached
gulp.task('purge', () => {
const list = [];
return gulp
.src(paths.upload)
.pipe(
through.obj((file, enc, cb) => {
const filename = file.path.split('/').pop();
list.push(`${versionPath}/${filename}`);
cb(null);
}),
)
.on('end', () => {
const purge = new FastlyPurge(fastly.token);
list.forEach(url => {
console.log(`Purging ${url}...`);
purge.url(url, (error, result) => {
if (error) {
console.log(error);
} else if (result) {
console.log(result);
}
});
});
});
});
// Publish to demo bucket
gulp.task('demo', () => {
if (!allowed.includes(branch.current)) {
console.error(`Must be on ${allowed.join(', ')} to publish! (current: ${branch.current})`);
if (!canDeploy()) {
return null;
}
console.log(`Uploading '${version}' demo to ${aws.demo.domain}...`);
// Replace versioned files in readme.md
gulp
.src([`${root}/readme.md`])
gulp.src([`${root}/readme.md`])
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest(root));
@@ -359,8 +396,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
pages.push(error);
}
gulp
.src(pages)
gulp.src(pages)
.pipe(replace(localPath, versionPath))
.pipe(s3(aws.demo, options.demo));
@@ -399,22 +435,28 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
}));
}); */
// Open the demo site to check it's sweet
gulp.task('open', () => {
console.log(`Opening ${aws.demo.domain}...`);
// A file must be specified or gulp will skip the task
// Doesn't matter which file since we set the URL above
// Weird, I know...
return gulp.src([`${paths.demo.root}index.html`]).pipe(
open('', {
url: `http://${aws.demo.domain}`,
// Open the demo site to check it's ok
gulp.task('open', callback => {
gulp.src(__filename).pipe(
open({
uri: `https://${aws.demo.domain}`,
}),
);
callback();
});
// Do everything
gulp.task('publish', callback => {
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo', callback);
});
gulp.task(
'deploy',
gulp.series(
'version',
tasks.clean,
gulp.parallel(tasks.js, tasks.sass, tasks.sprite),
'cdn',
'demo',
'purge',
'open',
),
);
}
+65 -54
View File
@@ -1,63 +1,27 @@
{
"name": "plyr",
"version": "3.3.12",
"version": "3.4.7",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>",
"keywords": [
"HTML5 Video",
"HTML5 Audio",
"Media Player",
"DASH",
"Shaka",
"WordPress",
"HLS"
],
"main": "./dist/plyr.js",
"browser": "./dist/plyr.min.js",
"sass": "./src/sass/plyr.scss",
"style": "./dist/plyr.css",
"devDependencies": {
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.7.0",
"del": "^3.0.0",
"eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.12.0",
"git-branch": "^2.0.1",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^5.0.0",
"gulp-better-rollup": "^3.1.0",
"gulp-clean-css": "^3.9.4",
"gulp-concat": "^2.6.1",
"gulp-filter": "^5.1.0",
"gulp-header": "^2.0.5",
"gulp-open": "^3.0.1",
"gulp-postcss": "^7.0.1",
"gulp-rename": "^1.2.3",
"gulp-replace": "^1.0.0",
"gulp-s3": "^0.11.0",
"gulp-sass": "^4.0.1",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.4",
"gulp-svgmin": "^1.2.4",
"gulp-svgstore": "^6.1.1",
"gulp-uglify-es": "^1.0.4",
"gulp-util": "^3.0.8",
"postcss-custom-properties": "^7.0.0",
"prettier-eslint": "^8.8.1",
"prettier-stylelint": "^0.4.2",
"rollup-plugin-babel": "^3.0.4",
"rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-node-resolve": "^3.3.0",
"run-sequence": "^2.2.1",
"stylelint": "^9.2.1",
"stylelint-config-prettier": "^3.2.0",
"stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.0.0",
"stylelint-order": "^0.8.1",
"stylelint-scss": "^3.1.0",
"stylelint-selector-bem-pattern": "^2.0.0"
},
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
"license": "MIT",
"repository": {
"type": "git",
"url": "git://github.com/sampotts/plyr.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/sampotts/plyr/issues"
},
@@ -66,15 +30,62 @@
},
"scripts": {
"build": "gulp build",
"lint": "eslint src/js",
"lint": "eslint src/js && npm run-script remark",
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Sam Potts <sam@potts.es>",
"devDependencies": {
"@babel/core": "^7.1.5",
"babel-eslint": "^10.0.1",
"@babel/preset-env": "^7.1.5",
"del": "^3.0.0",
"eslint": "^5.8.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^3.1.0",
"eslint-plugin-import": "^2.14.0",
"fastly-purge": "^1.0.1",
"git-branch": "^2.0.1",
"gulp": "^4.0.0",
"gulp-autoprefixer": "^6.0.0",
"gulp-better-rollup": "^3.4.0",
"gulp-clean-css": "^3.10.0",
"gulp-concat": "^2.6.1",
"gulp-filter": "^5.1.0",
"gulp-header": "^2.0.5",
"gulp-open": "^3.0.1",
"gulp-postcss": "^8.0.0",
"gulp-rename": "^1.4.0",
"gulp-replace": "^1.0.0",
"gulp-s3": "^0.11.0",
"gulp-sass": "^4.0.2",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.4",
"gulp-svgmin": "^2.1.0",
"gulp-svgstore": "^7.0.0",
"gulp-uglify-es": "^1.0.4",
"gulp-util": "^3.0.8",
"postcss-custom-properties": "^8.0.9",
"prettier-eslint": "^8.8.2",
"prettier-stylelint": "^0.4.2",
"remark-cli": "^6.0.0",
"remark-validate-links": "^7.1.0",
"rollup-plugin-babel": "^4.0.3",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-node-resolve": "^3.4.0",
"stylelint": "^9.7.1",
"stylelint-config-prettier": "^4.0.0",
"stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.2.0",
"stylelint-order": "^1.0.0",
"stylelint-scss": "^3.4.0",
"stylelint-selector-bem-pattern": "^2.0.0",
"through2": "^3.0.0"
},
"dependencies": {
"babel-polyfill": "^6.26.0",
"custom-event-polyfill": "^0.3.0",
"core-js": "^2.5.7",
"custom-event-polyfill": "^1.0.6",
"loadjs": "^3.5.4",
"raven-js": "^3.26.1",
"url-polyfill": "^1.0.13"
"raven-js": "^3.27.0",
"url-polyfill": "^1.1.0"
}
}
+2 -1
View File
@@ -11,7 +11,8 @@
},
// Exclude from search
"search.exclude": {
"dist/": true
"dist/": true,
"demo/dist/": true
},
// Linting
"stylelint.enable": true,
+99 -95
View File
@@ -8,26 +8,26 @@ A simple, lightweight, accessible and customizable HTML5, YouTube and Vimeo medi
## Features
* **Accessible** - full support for VTT captions and screen readers
* **[Customisable](#html)** - make the player look how you want with the markup you want
* **Good HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
- **Accessible** - full support for VTT captions and screen readers
- **[Customisable](#html)** - make the player look how you want with the markup you want
- **Good HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
`<span>` or `<a href="#">` button hacks
* **Responsive** - works with any screen size
* **HTML Video & Audio** - support for both formats
* **[Embedded Video](#embeds)** - support for YouTube and Vimeo video playback
* **[Monetization](#ads)** - make money from your videos
* **[Streaming](#streaming)** - support for hls.js, Shaka and dash.js streaming playback
* **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
* **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats
* **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
* **[Shortcuts](#shortcuts)** - supports keyboard shortcuts
* **Picture-in-Picture** - supports Safari's picture-in-picture mode
* **Playsinline** - supports the `playsinline` attribute
* **Speed controls** - adjust speed on the fly
* **Multiple captions** - support for multiple caption tracks
* **i18n support** - support for internationalization of controls
* **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required
* **SASS** - to include in your build processes
- **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](#try-plyr-online)** - support for hls.js, Shaka and dash.js streaming playback
- **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
- **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats
- **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
- **[Shortcuts](#shortcuts)** - supports keyboard shortcuts
- **Picture-in-Picture** - supports Safari's picture-in-picture mode
- **Playsinline** - supports the `playsinline` attribute
- **Speed controls** - adjust speed on the fly
- **Multiple captions** - support for multiple caption tracks
- **i18n support** - support for internationalization of controls
- **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required
- **SASS** - to include in your build processes
Oh and yes, it works with Bootstrap.
@@ -80,7 +80,7 @@ Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.o
</audio>
```
For YouTube and Vimeo players, Plyr uses progressive enhancement to enhance the default `<iframe>` embeds. Below are some examples. The `plyr__video-embed` classname will make the embed responsive. You can add the `autoplay`, `loop` and `playsinline` (YouTube only) query parameters to the URL and they will be set as config options automatically. For YouTube, the `origin` should be updated to reflect the domain you're hosting the embed on, or you can opt to omit it.
For YouTube and Vimeo players, Plyr uses progressive enhancement to enhance the default `<iframe>` embeds. Below are some examples. The `plyr__video-embed` classname will make the embed responsive. You can add the `autoplay`, `loop`, `hl` (YouTube only) and `playsinline` (YouTube only) query parameters to the URL and they will be set as config options automatically. For YouTube, the `origin` should be updated to reflect the domain you're hosting the embed on, or you can opt to omit it.
#### YouTube embed
@@ -132,13 +132,13 @@ See [initialising](#initialising) for more information on advanced setups.
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
```html
<script src="https://cdn.plyr.io/3.3.12/plyr.js"></script>
<script src="https://cdn.plyr.io/3.4.7/plyr.js"></script>
```
...or...
```html
<script src="https://cdn.plyr.io/3.3.12/plyr.polyfilled.js"></script>
<script src="https://cdn.plyr.io/3.4.7/plyr.polyfilled.js"></script>
```
### CSS
@@ -152,21 +152,21 @@ 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.3.12/plyr.css">
<link rel="stylesheet" href="https://cdn.plyr.io/3.4.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.3.12/plyr.svg`.
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.4.7/plyr.svg`.
## Ads
Plyr has partnered up with [vi.ai](http://vi.ai/publisher-video-monetization/?aid=plyrio) to offer monetization options for your videos. Getting setup is easy:
Plyr has partnered up with [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) to offer monetization options for your videos. Getting setup is easy:
* [Sign up for a vi.ai account](http://vi.ai/publisher-video-monetization/?aid=plyrio)
* Grab your publisher ID from the code snippet
* Enable ads in the [config options](#options) and enter your publisher ID
- [Sign up for a vi.ai account](https://vi.ai/publisher-video-monetization/?aid=plyrio)
- Grab your publisher ID from the code snippet
- Enable ads in the [config options](#options) and enter your publisher ID
Any questions regarding the ads can be sent straight to vi.ai and any issues with rendering raised through GitHub issues.
@@ -175,7 +175,7 @@ Any questions regarding the ads can be sent straight to vi.ai and any issues wit
### SASS
You can use `bundle.scss` file included in `/src` as part of your build and change variables to suit your design. The SASS require you to
use the [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) plugin (you be should already!) as all declarations use the W3C definitions.
use the [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) plugin (you should be already!) as all declarations use the W3C definitions.
The HTML markup uses the BEM methodology with `plyr` as the block, e.g. `.plyr__controls`. You can change the class hooks in the options to match any custom CSS
you write. Check out the JavaScript source for more on this.
@@ -213,10 +213,10 @@ WebVTT captions are supported. To add a caption track, check the HTML example ab
You can specify a range of arguments for the constructor to use:
* A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
* A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
* A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
* A [jQuery](https://jquery.com) object
- A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
- A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
- A [`NodeList`](https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
- A [jQuery](https://jquery.com) object
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below.
@@ -286,11 +286,11 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `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 |
| `i18n` | Object | See [defaults.js](/src/js/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
| `i18n` | Object | See [defaults.js](/src/js/config/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
| `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. |
| `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. |
| `blankUrl` | String | `https://cdn.plyr.io/static/blank.mp4` | Specify a URL or path to a blank video file used to properly cancel network requests. |
| `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. |
| `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. |
@@ -307,7 +307,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `invertTime` | Boolean | `true` | Display the current time as a countdown rather than an incremental counter. |
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
| `captions` | Object | `{ active: false, language: 'auto', update: false }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). 'auto' uses the browser language. `update`: Listen to changes to tracks and update menu. This is needed for some streaming libraries, but can result in unselectable language options). |
| `captions` | Object | `{ active: false, language: 'auto', update: false }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). 'auto' uses the browser language. `update`: Listen to changes to tracks and update menu. This is needed for some streaming libraries, but can result in unselectable language options). |
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) |
| `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. |
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
@@ -315,6 +315,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display. |
| `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 vi.ai ads. `publisherId`: Your unique vi.ai publisher ID. |
| `urls` | Object | See source. | If you wish to override any API URLs then you can do so here. You can also set a custom download URL for the download button. |
1. Vimeo only
@@ -365,8 +366,9 @@ player.fullscreen.enter(); // Enter fullscreen
| `fullscreen.exit()` | - | Exit fullscreen. |
| `fullscreen.toggle()` | - | Toggle fullscreen. |
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
| `once(event, function)` | String, Function | Add an event listener for the specified event once. |
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
| `supports(type)` | String | Check support for a mime type. |
| `destroy()` | - | Destroy the instance and garbage collect any elements. |
@@ -390,32 +392,32 @@ player.currentTime; // 10
player.fullscreen.active; // false;
```
| Property | Getter | Setter | Description |
| -------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isHTML5` | ✓ | - | Returns a boolean indicating if the current player is HTML5. |
| `isEmbed` | ✓ | - | Returns a boolean indicating if the current player is an embedded player. |
| `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. |
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
| `stopped` | ✓ | - | Returns a boolean indicating if the current player is stopped. |
| `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. |
| `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
| `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
| `seeking` | ✓ | - | Returns a boolean indicating if the current player is seeking. |
| `duration` | ✓ | - | Returns the duration for the current media. |
| `volume` | ✓ | ✓ | Gets or sets the volume for the player. The setter accepts a float between 0 and 1. |
| `muted` | ✓ | ✓ | Gets or sets the muted state of the player. The setter accepts a boolean. |
| `hasAudio` | ✓ | - | Returns a boolean indicating if the current media has an audio track. |
| `speed` | ✓ | ✓ | Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5. |
| `quality`&sup1; | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
| `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. |
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
| `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
| `currentTrack` | ✓ | ✓ | Gets or sets the caption track by index. `-1` means the track is missing or captions is not active |
| Property | Getter | Setter | Description |
| -------------------- | ------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `isHTML5` | ✓ | - | Returns a boolean indicating if the current player is HTML5. |
| `isEmbed` | ✓ | - | Returns a boolean indicating if the current player is an embedded player. |
| `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. |
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
| `stopped` | ✓ | - | Returns a boolean indicating if the current player is stopped. |
| `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. |
| `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
| `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
| `seeking` | ✓ | - | Returns a boolean indicating if the current player is seeking. |
| `duration` | ✓ | - | Returns the duration for the current media. |
| `volume` | ✓ | ✓ | Gets or sets the volume for the player. The setter accepts a float between 0 and 1. |
| `muted` | ✓ | ✓ | Gets or sets the muted state of the player. The setter accepts a boolean. |
| `hasAudio` | ✓ | - | Returns a boolean indicating if the current media has an audio track. |
| `speed` | ✓ | ✓ | Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5. |
| `quality`&sup1; | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
| `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. |
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#the-source-setter) below for examples. |
| `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
| `currentTrack` | ✓ | ✓ | Gets or sets the caption track by index. `-1` means the track is missing or captions is not active |
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. If your captions don't have any language data, or if you have multiple tracks with the same language, you may want to use `currentTrack` instead. |
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
| `pip` | ✓ | ✓ | 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+. |
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
| `pip`&sup2; | ✓ | ✓ | 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+. |
1. YouTube only. HTML5 will follow.
2. HTML5 only
@@ -434,10 +436,12 @@ player.source = {
{
src: '/path/to/movie.mp4',
type: 'video/mp4',
size: 720,
},
{
src: '/path/to/movie.webm',
type: 'video/webm',
size: 1080,
},
],
poster: '/path/to/poster.jpg',
@@ -562,6 +566,7 @@ player.on('ready', event => {
| `loadstart` | Sent when loading of the media begins. |
| `loadeddata` | The first frame of the media has finished loading. |
| `loadedmetadata` | The media's metadata has finished loading; all attributes now contain as much useful information as they're going to. |
| `qualitychange` | The quality of playback has changed. |
| `canplay` | Sent when enough data is available that the media can be played, at least for a couple of frames. This corresponds to the `HAVE_ENOUGH_DATA` `readyState`. |
| `canplaythrough` | Sent when the ready state changes to `CAN_PLAY_THROUGH`, indicating that the entire media can be played without interruption, assuming the download rate remains at least at the current level. _Note:_ Manually setting the `currentTime` will eventually fire a `canplaythrough` event in firefox. Other browsers might not fire this event. |
| `stalled` | Sent when the user agent is trying to fetch media data, but data is unexpectedly not forthcoming. |
@@ -572,11 +577,9 @@ player.on('ready', event => {
#### YouTube only
| Event Type | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `statechange` | The state of the player has changed. The code can be accessed via `event.detail.code`. Possible values are `-1`: Unstarted, `0`: Ended, `1`: Playing, `2`: Paused, `3`: Buffering, `5`: Video cued. See the [YouTube Docs](https://developers.google.com/youtube/iframe_api_reference#onStateChange) for more information. |
| `qualitychange` | The quality of playback has changed. |
| `qualityrequested` | A change to playback quality has been requested. _Note:_ A change to quality can only be _requested_ via the API. There is no guarantee the quality will change to the level requested. You should listen to the `qualitychange` event for true changes. |
| Event Type | Description |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `statechange` | The state of the player has changed. The code can be accessed via `event.detail.code`. Possible values are `-1`: Unstarted, `0`: Ended, `1`: Playing, `2`: Paused, `3`: Buffering, `5`: Video cued. See the [YouTube Docs](https://developers.google.com/youtube/iframe_api_reference#onStateChange) for more information. |
_Note:_ These events also bubble up the DOM. The event target will be the container element.
@@ -588,8 +591,8 @@ YouTube and Vimeo are currently supported and function much like a HTML5 video.
to access the API's directly. You can do so via the `embed` property of your player object - e.g. `player.embed`. You can then use the relevant methods from the
third party APIs. More info on the respective API's here:
* [YouTube iframe API Reference](https://developers.google.com/youtube/iframe_api_reference)
* [Vimeo player.js Reference](https://github.com/vimeo/player.js)
- [YouTube iframe API Reference](https://developers.google.com/youtube/iframe_api_reference)
- [Vimeo player.js Reference](https://github.com/vimeo/player.js)
_Note_: Not all API methods may work 100%. Your mileage may vary. It's better to use the Plyr API where possible.
@@ -649,9 +652,9 @@ const supported = Plyr.supported('video', 'html5', true);
The arguments are:
* Media type (`audio` or `video`)
* Provider (`html5`, `youtube` or `vimeo`)
* Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
- Media type (`audio` or `video`)
- Provider (`html5`, `youtube` or `vimeo`)
- Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
### Disable support programatically
@@ -684,33 +687,34 @@ Plyr is developed by [@sam_potts](https://twitter.com/sam_potts) / [sampotts.me]
Plyr costs money to run, not only my time. I donate my time for free as I enjoy building Plyr but unfortunately have to pay for domains, hosting, and more. Any help with costs is appreciated...
* [Donate via Patron](https://www.patreon.com/plyr)
* [Donate via PayPal](https://www.paypal.me/pottsy/20usd)
- [Donate via Patreon](https://www.patreon.com/plyr)
- [Donate via PayPal](https://www.paypal.me/pottsy/20usd)
## Mentions
* [ProductHunt](https://www.producthunt.com/tech/plyr)
* [The Changelog](http://thechangelog.com/plyr-simple-html5-media-player-custom-controls-webvtt-captions/)
* [HTML5 Weekly #177](http://html5weekly.com/issues/177)
* [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f)
* [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/)
* [Hacker News](https://news.ycombinator.com/item?id=9136774)
* [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04)
* [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player)
* [The Treehouse Show #131](https://teamtreehouse.com/library/episode-131-origami-react-responsive-hero-images)
* [noupe.com](http://www.noupe.com/design/html5-plyr-is-a-responsive-and-accessible-video-player-94389.html)
- [ProductHunt](https://www.producthunt.com/tech/plyr)
- [The Changelog](http://thechangelog.com/plyr-simple-html5-media-player-custom-controls-webvtt-captions/)
- [HTML5 Weekly #177](http://html5weekly.com/issues/177)
- [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f)
- [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/)
- [Hacker News](https://news.ycombinator.com/item?id=9136774)
- [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04)
- [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player)
- [The Treehouse Show #131](https://teamtreehouse.com/library/episode-131-origami-react-responsive-hero-images)
- [noupe.com](http://www.noupe.com/design/html5-plyr-is-a-responsive-and-accessible-video-player-94389.html)
## Used by
* [Selz.com](https://selz.com)
* [Peugeot.fr](http://www.peugeot.fr/marque-et-technologie/technologies/peugeot-i-cockpit.html)
* [Peugeot.de](http://www.peugeot.de/modelle/modellberater/208-3-turer/fotos-videos.html)
* [TomTom.com](http://prioritydriving.tomtom.com/)
* [DIGBMX](http://digbmx.com/)
* [Grime Archive](https://grimearchive.com/)
* [koel - A personal music streaming server that works.](http://koel.phanan.net/)
* [Oscar Radio](http://oscar-radio.xyz/)
* [Sparkk TV](https://www.sparkktv.com/)
- [Selz.com](https://selz.com)
- [Peugeot.fr](http://www.peugeot.fr/marque-et-technologie/technologies/peugeot-i-cockpit.html)
- [Peugeot.de](http://www.peugeot.de/modelle/modellberater/208-3-turer/fotos-videos.html)
- [TomTom.com](http://prioritydriving.tomtom.com/)
- [DIGBMX](http://digbmx.com/)
- [Grime Archive](https://grimearchive.com/)
- [koel - A personal music streaming server that works.](http://koel.phanan.net/)
- [Oscar Radio](http://oscar-radio.xyz/)
- [Sparkk TV](https://www.sparkktv.com/)
- [@halfhalftravel](https://www.halfhalftravel.com/)
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 :-)
@@ -718,8 +722,8 @@ Let me know on [Twitter](https://twitter.com/sam_potts) I can add you to the abo
Credit to the PayPal HTML5 Video player from which Plyr's caption functionality was originally ported from:
* [PayPal's Accessible HTML5 Video Player](https://github.com/paypal/accessible-html5-video-player)
* [An awesome guide for Plyr in Japanese!](http://syncer.jp/how-to-use-plyr-io) by [@arayutw](https://twitter.com/arayutw)
- [PayPal's Accessible HTML5 Video Player](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
+181 -86
View File
@@ -4,9 +4,23 @@
// ==========================================================================
import controls from './controls';
import i18n from './i18n';
import support from './support';
import utils from './utils';
import { dedupe } from './utils/arrays';
import browser from './utils/browser';
import {
createElement,
emptyElement,
getAttributesFromSelector,
insertAfter,
removeElement,
toggleClass,
} from './utils/elements';
import { on, triggerEvent } from './utils/events';
import fetch from './utils/fetch';
import i18n from './utils/i18n';
import is from './utils/is';
import { getHTML } from './utils/strings';
import { parseUrl } from './utils/urls';
const captions = {
// Setup captions
@@ -19,7 +33,11 @@ const captions = {
// Only Vimeo and HTML5 video supported at this point
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
// Clear menu and hide
if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
if (
is.array(this.config.controls) &&
this.config.controls.includes('settings') &&
this.config.settings.includes('captions')
) {
controls.setCaptionsMenu.call(this);
}
@@ -27,15 +45,12 @@ const captions = {
}
// Inject the container
if (!utils.is.element(this.elements.captions)) {
this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions));
if (!is.element(this.elements.captions)) {
this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
utils.insertAfter(this.elements.captions, this.elements.wrapper);
insertAfter(this.elements.captions, this.elements.wrapper);
}
// Get browser info
const browser = utils.getBrowser();
// Fix IE captions if CORS is used
// Fetch captions and inject as blobs instead (data URIs not supported!)
if (browser.isIE && window.URL) {
@@ -43,84 +58,95 @@ const captions = {
Array.from(elements).forEach(track => {
const src = track.getAttribute('src');
const href = utils.parseUrl(src);
const url = parseUrl(src);
if (href.hostname !== window.location.href.hostname && [
'http:',
'https:',
].includes(href.protocol)) {
utils
.fetch(src, 'blob')
if (
url !== null &&
url.hostname !== window.location.href.hostname &&
['http:', 'https:'].includes(url.protocol)
) {
fetch(src, 'blob')
.then(blob => {
track.setAttribute('src', window.URL.createObjectURL(blob));
})
.catch(() => {
utils.removeElement(track);
removeElement(track);
});
}
});
}
// Try to load the value from storage
let active = this.storage.get('captions');
// Get and set initial data
// The "preferred" options are not realized unless / until the wanted language has a match
// * languages: Array of user's browser languages.
// * language: The language preferred by user settings or config
// * active: The state preferred by user settings or config
// * toggled: The real captions state
// Otherwise fall back to the default config
if (!utils.is.boolean(active)) {
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
// Use first browser language when language is 'auto'
if (language === 'auto') {
[language] = languages;
}
let active = this.storage.get('captions');
if (!is.boolean(active)) {
({ active } = this.config.captions);
}
// Get language from storage, fallback to config
let language = this.storage.get('language') || this.config.captions.language;
if (language === 'auto') {
[ language ] = (navigator.language || navigator.userLanguage).split('-');
}
// Set language and show if active
captions.setLanguage.call(this, language, active);
Object.assign(this.captions, {
toggled: false,
active,
language,
languages,
});
// Watch changes to textTracks and update captions menu
if (this.isHTML5) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
utils.on(this.media.textTracks, trackEvents, captions.update.bind(this));
on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
}
// Update available languages in list next tick (the event must not be triggered before the listeners)
setTimeout(captions.update.bind(this), 0);
},
// Update available language options in settings based on tracks
update() {
const tracks = captions.getTracks.call(this, true);
// Get the wanted language
const { language, meta } = this.captions;
const { active, language, meta, currentTrackNode } = this.captions;
const languageExists = Boolean(tracks.find(track => track.language === language));
// Handle tracks (add event listener and "pseudo"-default)
if (this.isHTML5 && this.isVideo) {
tracks
.filter(track => !meta.get(track))
.forEach(track => {
this.debug.log('Track added', track);
// Attempt to store if the original dom element was "default"
meta.set(track, {
default: track.mode === 'showing',
});
// Turn off native caption rendering to avoid double captions
track.mode = 'hidden';
// Add event listener for cue changes
utils.on(track, 'cuechange', () => captions.updateCues.call(this));
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
track.mode = 'hidden';
// Add event listener for cue changes
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
});
}
const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode);
const firstMatch = this.language !== language && tracks.find(track => track.language === language);
// Update language if removed or first matching track added
if (trackRemoved || firstMatch) {
captions.setLanguage.call(this, language, this.config.captions.active);
// Update language first time it matches, or if the previous matching track was removed
if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
captions.setLanguage.call(this, language);
captions.toggle.call(this, active && languageExists);
}
// Enable or disable captions based on track length
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(tracks));
toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
// Update available languages in list
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
@@ -128,16 +154,72 @@ const captions = {
}
},
set(index, setLanguage = true, show = true) {
// Toggle captions display
// Used internally for the toggleCaptions method, with the passive option forced to false
toggle(input, passive = true) {
// If there's no full support
if (!this.supported.ui) {
return;
}
const { toggled } = this.captions; // Current state
const activeClass = this.config.classNames.captions.active;
// Get the next state
// If the method is called without parameter, toggle based on current value
const active = is.nullOrUndefined(input) ? !toggled : input;
// Update state and trigger event
if (active !== toggled) {
// When passive, don't override user preferences
if (!passive) {
this.captions.active = active;
this.storage.set({ captions: active });
}
// Force language if the call isn't passive and there is no matching language to toggle to
if (!this.language && active && !passive) {
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);
// Override user preferences to avoid switching languages if a matching track is added
this.captions.language = track.language;
// Set caption, but don't store in localStorage as user preference
captions.set.call(this, tracks.indexOf(track));
return;
}
// Toggle button if it's enabled
if (this.elements.buttons.captions) {
this.elements.buttons.captions.pressed = active;
}
// Add class hook
toggleClass(this.elements.container, activeClass, active);
this.captions.toggled = active;
// Update settings menu
controls.updateSetting.call(this, 'captions');
// Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
}
},
// Set captions by track index
// Used internally for the currentTrack setter with the passive option forced to false
set(index, passive = true) {
const tracks = captions.getTracks.call(this);
// Disable captions if setting to -1
if (index === -1) {
this.toggleCaptions(false);
captions.toggle.call(this, false, passive);
return;
}
if (!utils.is.number(index)) {
if (!is.number(index)) {
this.debug.warn('Invalid caption argument', index);
return;
}
@@ -149,15 +231,19 @@ const captions = {
if (this.captions.currentTrack !== index) {
this.captions.currentTrack = index;
const track = captions.getCurrentTrack.call(this);
const track = tracks[index];
const { language } = track || {};
// Store reference to node for invalidation on remove
this.captions.currentTrackNode = track;
// Prevent setting language in some cases, since it can violate user's intentions
if (setLanguage) {
// Update settings menu
controls.updateSetting.call(this, 'captions');
// When passive, don't override user preferences
if (!passive) {
this.captions.language = language;
this.storage.set({ language });
}
// Handle Vimeo captions
@@ -166,32 +252,33 @@ const captions = {
}
// Trigger event
utils.dispatchEvent.call(this, this.media, 'languagechange');
triggerEvent.call(this, this.media, 'languagechange');
}
// Show captions
captions.toggle.call(this, true, passive);
if (this.isHTML5 && this.isVideo) {
// If we change the active track while a cue is already displayed we need to update it
captions.updateCues.call(this);
}
// Show captions
if (show) {
this.toggleCaptions(true);
}
},
setLanguage(language, show = true) {
if (!utils.is.string(language)) {
this.debug.warn('Invalid language argument', language);
// Set captions by language
// Used internally for the language setter with the passive option forced to false
setLanguage(input, passive = true) {
if (!is.string(input)) {
this.debug.warn('Invalid language argument', input);
return;
}
// Normalize
this.captions.language = language.toLowerCase();
const language = input.toLowerCase();
this.captions.language = language;
// Set currentTrack
const tracks = captions.getTracks.call(this);
const track = captions.getCurrentTrack.call(this, true);
captions.set.call(this, tracks.indexOf(track), false, show);
const track = captions.findTrack.call(this, [language]);
captions.set.call(this, tracks.indexOf(track), passive);
},
// Get current valid caption tracks
@@ -204,34 +291,42 @@ const captions = {
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
.filter(track => [
'captions',
'subtitles',
].includes(track.kind));
.filter(track => ['captions', 'subtitles'].includes(track.kind));
},
// Get the current track for the current language
getCurrentTrack(fromLanguage = false) {
// Match tracks based on languages and get the first
findTrack(languages, force = false) {
const tracks = captions.getTracks.call(this);
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
return (!fromLanguage && tracks[this.currentTrack]) || sorted.find(track => track.language === this.captions.language) || sorted[0];
let track;
languages.every(language => {
track = sorted.find(track => track.language === language);
return !track; // Break iteration if there is a match
});
// If no match is found but is required, get first
return track || (force ? sorted[0] : undefined);
},
// Get the current track
getCurrentTrack() {
return captions.getTracks.call(this)[this.currentTrack];
},
// Get UI label for track
getLabel(track) {
let currentTrack = track;
if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) {
if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
currentTrack = captions.getCurrentTrack.call(this);
}
if (utils.is.track(currentTrack)) {
if (!utils.is.empty(currentTrack.label)) {
if (is.track(currentTrack)) {
if (!is.empty(currentTrack.label)) {
return currentTrack.label;
}
if (!utils.is.empty(currentTrack.language)) {
if (!is.empty(currentTrack.language)) {
return track.language.toUpperCase();
}
@@ -249,13 +344,13 @@ const captions = {
return;
}
if (!utils.is.element(this.elements.captions)) {
if (!is.element(this.elements.captions)) {
this.debug.warn('No captions element to render to');
return;
}
// Only accept array or empty input
if (!utils.is.nullOrUndefined(input) && !Array.isArray(input)) {
if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
this.debug.warn('updateCues: Invalid input', input);
return;
}
@@ -267,7 +362,7 @@ const captions = {
const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || [])
.map(cue => cue.getCueAsHTML())
.map(utils.getHTML);
.map(getHTML);
}
// Set new caption text
@@ -276,13 +371,13 @@ const captions = {
if (changed) {
// Empty the container and create a new child element
utils.emptyElement(this.elements.captions);
const caption = utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.caption));
emptyElement(this.elements.captions);
const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
caption.innerHTML = content;
this.elements.captions.appendChild(caption);
// Trigger event
utils.dispatchEvent.call(this, this.media, 'cuechange');
triggerEvent.call(this, this.media, 'cuechange');
}
},
};
@@ -18,6 +18,10 @@ const defaults = {
// Only allow one media playing at once (vimeo only)
autopause: true,
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
playsinline: true,
// Default time to skip when rewind/fast forward
seekTime: 10,
@@ -56,7 +60,7 @@ const defaults = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.3.12/plyr.svg',
iconUrl: 'https://cdn.plyr.io/3.4.7/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@@ -64,19 +68,7 @@ const defaults = {
// Quality default
quality: {
default: 576,
options: [
4320,
2880,
2160,
1440,
1080,
720,
576,
480,
360,
240,
'default', // YouTube's "auto"
],
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
},
// Set loops
@@ -89,15 +81,7 @@ const defaults = {
// Speed default and options to display
speed: {
selected: 1,
options: [
0.5,
0.75,
1,
1.25,
1.5,
1.75,
2,
],
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
},
// Keyboard shortcut settings
@@ -149,13 +133,10 @@ const defaults = {
'settings',
'pip',
'airplay',
// 'download',
'fullscreen',
],
settings: [
'captions',
'quality',
'speed',
],
settings: ['captions', 'quality', 'speed'],
// Localisation
i18n: {
@@ -165,6 +146,7 @@ const defaults = {
pause: 'Pause',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
@@ -174,11 +156,13 @@ const defaults = {
unmute: 'Unmute',
enableCaptions: 'Enable captions',
disableCaptions: 'Disable captions',
download: 'Download',
enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen',
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
@@ -202,6 +186,7 @@ const defaults = {
// URLs
urls: {
download: null,
vimeo: {
sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}',
@@ -209,7 +194,8 @@ 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://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
},
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
@@ -227,6 +213,7 @@ const defaults = {
mute: null,
volume: null,
captions: null,
download: null,
fullscreen: null,
pip: null,
airplay: null,
@@ -262,6 +249,7 @@ const defaults = {
'cuechange',
// Custom events
'download',
'enterfullscreen',
'exitfullscreen',
'captionsenabled',
@@ -273,8 +261,9 @@ const defaults = {
// YouTube
'statechange',
// Quality
'qualitychange',
'qualityrequested',
// Ads
'adsloaded',
@@ -306,6 +295,7 @@ const defaults = {
fastForward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]',
download: '[data-plyr="download"]',
fullscreen: '[data-plyr="fullscreen"]',
pip: '[data-plyr="pip"]',
airplay: '[data-plyr="airplay"]',
@@ -345,6 +335,7 @@ const defaults = {
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
@@ -358,6 +349,9 @@ const defaults = {
isTouch: 'plyr--is-touch',
uiSupported: 'plyr--full-ui',
noTransition: 'plyr--no-transition',
display: {
time: 'plyr__time',
},
menu: {
value: 'plyr__menu__value',
badge: 'plyr__badge',
+10
View File
@@ -0,0 +1,10 @@
// ==========================================================================
// Plyr states
// ==========================================================================
export const pip = {
active: 'picture-in-picture',
inactive: 'inline',
};
export default { pip };
@@ -13,4 +13,22 @@ export const types = {
video: 'video',
};
/**
* Get provider by URL
* @param {String} url
*/
export function getProviderByUrl(url) {
// YouTube
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) {
return providers.youtube;
}
// Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo;
}
return null;
}
export default { providers, types };
+2
View File
@@ -17,10 +17,12 @@ export default class Console {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
}
get warn() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
}
get error() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
+778 -553
View File
File diff suppressed because it is too large Load Diff
+70 -30
View File
@@ -1,11 +1,14 @@
// ==========================================================================
// Fullscreen wrapper
// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing
// https://webkit.org/blog/7929/designing-websites-for-iphone-x/
// ==========================================================================
import utils from './utils';
const browser = utils.getBrowser();
import { repaint } from './utils/animation';
import browser from './utils/browser';
import { hasClass, toggleClass, trapFocus } from './utils/elements';
import { on, triggerEvent } from './utils/events';
import is from './utils/is';
function onChange() {
if (!this.enabled) {
@@ -14,16 +17,16 @@ function onChange() {
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (utils.is.element(button)) {
utils.toggleState(button, this.active);
if (is.element(button)) {
button.pressed = this.active;
}
// Trigger an event
utils.dispatchEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
// Trap focus in container
if (!browser.isIos) {
utils.trapFocus.call(this.player, this.target, this.active);
trapFocus.call(this.player, this.target, this.active);
}
}
@@ -42,7 +45,38 @@ function toggleFallback(toggle = false) {
document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook
utils.toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
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);
@@ -62,15 +96,20 @@ class Fullscreen {
// Register event listeners
// Handle event (incase user presses escape etc)
utils.on(document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {
// TODO: Filter for target??
onChange.call(this);
});
on.call(
this.player,
document,
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => {
// TODO: Filter for target??
onChange.call(this);
},
);
// Fullscreen toggle on double click
utils.on(this.player.elements.container, 'dblclick', event => {
on.call(this.player, this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls
if (utils.is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return;
}
@@ -83,26 +122,27 @@ class Fullscreen {
// Determine if native supported
static get native() {
return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);
return !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled
);
}
// Get the prefix for handlers
static get prefix() {
// No prefix
if (utils.is.function(document.exitFullscreen)) {
if (is.function(document.exitFullscreen)) {
return '';
}
// Check for fullscreen support by vendor prefix
let value = '';
const prefixes = [
'webkit',
'moz',
'ms',
];
const prefixes = ['webkit', 'moz', 'ms'];
prefixes.some(pre => {
if (utils.is.function(document[`${pre}ExitFullscreen`]) || utils.is.function(document[`${pre}CancelFullScreen`])) {
if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
value = pre;
return true;
}
@@ -135,7 +175,7 @@ class Fullscreen {
// Fallback using classname
if (!Fullscreen.native) {
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
}
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
@@ -145,7 +185,9 @@ class Fullscreen {
// Get target element
get target() {
return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container;
return browser.isIos && this.player.config.fullscreen.iosNative
? this.player.media
: this.player.elements.container;
}
// Update UI
@@ -157,7 +199,7 @@ class Fullscreen {
}
// Add styling hook to show button
utils.toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
}
// Make an element fullscreen
@@ -168,14 +210,12 @@ class Fullscreen {
// iOS native fullscreen doesn't need the request step
if (browser.isIos && this.player.config.fullscreen.iosNative) {
if (this.player.playing) {
this.target.webkitEnterFullscreen();
}
this.target.webkitEnterFullscreen();
} else if (!Fullscreen.native) {
toggleFallback.call(this, true);
} else if (!this.prefix) {
this.target.requestFullscreen();
} else if (!utils.is.empty(this.prefix)) {
} else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`]();
}
}
@@ -194,7 +234,7 @@ class Fullscreen {
toggleFallback.call(this, false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!utils.is.empty(this.prefix)) {
} else if (!is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.property}`]();
}
+36 -72
View File
@@ -3,40 +3,28 @@
// ==========================================================================
import support from './support';
import utils from './utils';
import { removeElement } from './utils/elements';
import { triggerEvent } from './utils/events';
const html5 = {
getSources() {
if (!this.isHTML5) {
return null;
return [];
}
return this.media.querySelectorAll('source');
const sources = Array.from(this.media.querySelectorAll('source'));
// Filter out unsupported sources
return sources.filter(source => support.mime.call(this, source.getAttribute('type')));
},
// Get quality levels
getQualityOptions() {
if (!this.isHTML5) {
return null;
}
// Get sources
const sources = html5.getSources.call(this);
if (utils.is.empty(sources)) {
return null;
}
// Get <source> with size attribute
const sizes = Array.from(sources).filter(source => !utils.is.empty(source.getAttribute('size')));
// If none, bail
if (utils.is.empty(sizes)) {
return null;
}
// Reduce to unique list
return utils.dedupe(sizes.map(source => Number(source.getAttribute('size'))));
// Get sizes from <source> elements
return html5.getSources
.call(this)
.map(source => Number(source.getAttribute('size')))
.filter(Boolean);
},
extend() {
@@ -51,71 +39,47 @@ const html5 = {
get() {
// Get sources
const sources = html5.getSources.call(player);
const source = sources.find(source => source.getAttribute('src') === player.source);
if (utils.is.empty(sources)) {
return null;
}
const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
if (utils.is.empty(matches)) {
return null;
}
return Number(matches[0].getAttribute('size'));
// Return size, if match is found
return source && Number(source.getAttribute('size'));
},
set(input) {
// Get sources
const sources = html5.getSources.call(player);
if (utils.is.empty(sources)) {
// Get first match for requested size
const source = sources.find(source => Number(source.getAttribute('size')) === input);
// No matching source found
if (!source) {
return;
}
// Get matches for requested size
const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input);
// No matches for requested size
if (utils.is.empty(matches)) {
return;
}
// Get supported sources
const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type')));
// No supported sources
if (utils.is.empty(supported)) {
return;
}
// Trigger change event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
quality: input,
});
// Get current state
const { currentTime, playing } = player;
const { currentTime, paused, preload, readyState } = player.media;
// Set new source
player.media.src = supported[0].getAttribute('src');
player.media.src = source.getAttribute('src');
// Restore time
const onLoadedMetaData = () => {
player.currentTime = currentTime;
player.off('loadedmetadata', onLoadedMetaData);
};
player.on('loadedmetadata', onLoadedMetaData);
// 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;
// Load new source
player.media.load();
// Resume playing
if (!paused) {
player.play();
}
});
// Resume playing
if (playing) {
player.play();
// Load new source
player.media.load();
}
// Trigger change event
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: input,
});
},
@@ -130,7 +94,7 @@ const html5 = {
}
// Remove child sources
utils.removeElement(html5.getSources());
removeElement(html5.getSources.call(this));
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
-35
View File
@@ -1,35 +0,0 @@
// ==========================================================================
// Plyr internationalization
// ==========================================================================
import utils from './utils';
const i18n = {
get(key = '', config = {}) {
if (utils.is.empty(key) || utils.is.empty(config)) {
return '';
}
let string = utils.getDeep(config.i18n, key);
if (utils.is.empty(string)) {
return '';
}
const replace = {
'{seektime}': config.seekTime,
'{title}': config.title,
};
Object.entries(replace).forEach(([
key,
value,
]) => {
string = utils.replaceAll(string, key, value);
});
return string;
},
};
export default i18n;
+394 -358
View File
File diff suppressed because it is too large Load Diff
+12 -21
View File
@@ -5,7 +5,7 @@
import html5 from './html5';
import vimeo from './plugins/vimeo';
import youtube from './plugins/youtube';
import utils from './utils';
import { createElement, toggleClass, wrap } from './utils/elements';
const media = {
// Setup media
@@ -17,50 +17,41 @@ const media = {
}
// Add type class
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
// Add provider class
utils.toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
// Add video class for embeds
// This will require changes if audio embeds are added
if (this.isEmbed) {
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
}
// Inject the player wrapper
if (this.isVideo) {
// Create the wrapper div
this.elements.wrapper = utils.createElement('div', {
this.elements.wrapper = createElement('div', {
class: this.config.classNames.video,
});
// Wrap the video in a container
utils.wrap(this.media, this.elements.wrapper);
wrap(this.media, this.elements.wrapper);
// Faux poster container
this.elements.poster = utils.createElement('div', {
this.elements.poster = createElement('div', {
class: this.config.classNames.poster,
});
this.elements.wrapper.appendChild(this.elements.poster);
}
if (this.isEmbed) {
switch (this.provider) {
case 'youtube':
youtube.setup.call(this);
break;
case 'vimeo':
vimeo.setup.call(this);
break;
default:
break;
}
} else if (this.isHTML5) {
if (this.isHTML5) {
html5.extend.call(this);
} else if (this.isYouTube) {
youtube.setup.call(this);
} else if (this.isVimeo) {
vimeo.setup.call(this);
}
},
};
+35 -24
View File
@@ -6,8 +6,13 @@
/* global google */
import i18n from '../i18n';
import utils from '../utils';
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 { formatTime } from '../utils/time';
import { buildUrlParams } from '../utils/urls';
class Ads {
/**
@@ -44,7 +49,9 @@ class Ads {
}
get enabled() {
return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId);
return (
this.player.isHTML5 && this.player.isVideo && this.player.config.ads.enabled && !is.empty(this.publisherId)
);
}
/**
@@ -53,9 +60,8 @@ class Ads {
load() {
if (this.enabled) {
// Check if the Google IMA3 SDK is loaded or load it ourselves
if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
utils
.loadScript(this.player.config.urls.googleIMA.sdk)
if (!is.object(window.google) || !is.object(window.google.ima)) {
loadScript(this.player.config.urls.googleIMA.sdk)
.then(() => {
this.ready();
})
@@ -94,7 +100,7 @@ class Ads {
const params = {
AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
AV_CHANNELID: '5a0458dc28a06145e4519d21',
AV_URL: location.hostname,
AV_URL: window.location.hostname,
cb: Date.now(),
AV_WIDTH: 640,
AV_HEIGHT: 480,
@@ -103,7 +109,7 @@ class Ads {
const base = 'https://go.aniview.com/api/adserver6/vast/';
return `${base}?${utils.buildUrlParams(params)}`;
return `${base}?${buildUrlParams(params)}`;
}
/**
@@ -116,7 +122,7 @@ class Ads {
*/
setupIMA() {
// Create the container for our advertisements
this.elements.container = utils.createElement('div', {
this.elements.container = createElement('div', {
class: this.player.config.classNames.ads,
});
this.player.elements.container.appendChild(this.elements.container);
@@ -146,7 +152,11 @@ class Ads {
this.loader = new google.ima.AdsLoader(this.elements.displayContainer);
// Listen and respond to ads loaded and error events
this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false);
this.loader.addEventListener(
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
event => this.onAdsManagerLoaded(event),
false,
);
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);
// Request video ads
@@ -184,7 +194,7 @@ class Ads {
}
const update = () => {
const time = utils.formatTime(Math.max(this.manager.getRemainingTime(), 0));
const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));
const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;
this.elements.container.setAttribute('data-badge-text', label);
};
@@ -197,6 +207,11 @@ class Ads {
* @param {Event} adsManagerLoadedEvent
*/
onAdsManagerLoaded(event) {
// Load could occur after a source change (race condition)
if (!this.enabled) {
return;
}
// Get the ads manager
const settings = new google.ima.AdsRenderingSettings();
@@ -212,14 +227,14 @@ class Ads {
this.cuePoints = this.manager.getCuePoints();
// Add advertisement cue's within the time line if available
if (!utils.is.empty(this.cuePoints)) {
if (!is.empty(this.cuePoints)) {
this.cuePoints.forEach(cuePoint => {
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
const seekElement = this.player.elements.progress;
if (utils.is.element(seekElement)) {
if (is.element(seekElement)) {
const cuePercentage = 100 / this.player.duration * cuePoint;
const cue = utils.createElement('span', {
const cue = createElement('span', {
class: this.player.config.classNames.cues,
});
@@ -230,10 +245,6 @@ class Ads {
});
}
// Get skippable state
// TODO: Skip button
// this.player.debug.warn(this.manager.getAdSkippableState());
// Set volume to match player
this.manager.setVolume(this.player.volume);
@@ -266,7 +277,7 @@ class Ads {
// Proxy event
const dispatchEvent = type => {
const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
utils.dispatchEvent.call(this.player, this.player.media, event);
triggerEvent.call(this.player, this.player.media, event);
};
switch (event.type) {
@@ -393,7 +404,7 @@ class Ads {
this.player.on('seeked', () => {
const seekedTime = this.player.currentTime;
if (utils.is.empty(this.cuePoints)) {
if (is.empty(this.cuePoints)) {
return;
}
@@ -530,9 +541,9 @@ class Ads {
trigger(event, ...args) {
const handlers = this.events[event];
if (utils.is.array(handlers)) {
if (is.array(handlers)) {
handlers.forEach(handler => {
if (utils.is.function(handler)) {
if (is.function(handler)) {
handler.apply(this, args);
}
});
@@ -546,7 +557,7 @@ class Ads {
* @return {Ads}
*/
on(event, callback) {
if (!utils.is.array(this.events[event])) {
if (!is.array(this.events[event])) {
this.events[event] = [];
}
@@ -577,7 +588,7 @@ class Ads {
* @param {string} from
*/
clearSafetyTimer(from) {
if (!utils.is.nullOrUndefined(this.safetyTimer)) {
if (!is.nullOrUndefined(this.safetyTimer)) {
this.player.debug.log(`Safety timer cleared from: ${from}`);
clearTimeout(this.safetyTimer);
+90 -46
View File
@@ -2,10 +2,37 @@
// Vimeo plugin
// ==========================================================================
import captions from './../captions';
import controls from './../controls';
import ui from './../ui';
import utils from './../utils';
import captions from '../captions';
import controls from '../controls';
import ui from '../ui';
import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import loadScript from '../utils/loadScript';
import { format, stripHTML } from '../utils/strings';
import { buildUrlParams } from '../utils/urls';
// Parse Vimeo ID from URL
function parseId(url) {
if (is.empty(url)) {
return null;
}
if (is.number(Number(url))) {
return url;
}
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
return url.match(regex) ? RegExp.$2 : url;
}
// Get aspect ratio for dimensions
function getAspectRatio(width, height) {
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
const ratio = getRatio(width, height);
return `${width / ratio}:${height / ratio}`;
}
// Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) {
@@ -14,22 +41,21 @@ function assurePlaybackState(play) {
}
if (this.media.paused === play) {
this.media.paused = !play;
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
const vimeo = {
setup() {
// Add embed class for responsive
utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set intial ratio
vimeo.setAspectRatio.call(this);
// Load the API if not already
if (!utils.is.object(window.Vimeo)) {
utils
.loadScript(this.config.urls.vimeo.sdk)
if (!is.object(window.Vimeo)) {
loadScript(this.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(this);
})
@@ -44,8 +70,9 @@ const vimeo = {
// Set aspect ratio
// For Vimeo we have an extra 300% height <div> to hide the standard controls and UI
setAspectRatio(input) {
const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':');
const padding = 100 / ratio[0] * ratio[1];
const [x, y] = (is.string(input) ? input : this.config.ratio).split(':').map(Number);
const padding = (100 / x) * y;
vimeo.padding = padding;
this.elements.wrapper.style.paddingBottom = `${padding}%`;
if (this.supported.ui) {
@@ -73,34 +100,37 @@ const vimeo = {
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
};
const params = utils.buildUrlParams(options);
const params = buildUrlParams(options);
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (utils.is.empty(source)) {
if (is.empty(source)) {
source = player.media.getAttribute(player.config.attributes.embed.id);
}
const id = utils.parseVimeoId(source);
const id = parseId(source);
// Build an iframe
const iframe = utils.createElement('iframe');
const src = utils.format(player.config.urls.vimeo.iframe, id, params);
const iframe = createElement('iframe');
const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay');
// Get poster, if already set
const { poster } = player;
// Inject the package
const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer });
const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe);
player.media = utils.replaceElement(wrapper, player.media);
player.media = replaceElement(wrapper, player.media);
// Get poster image
utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (utils.is.empty(response)) {
fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (is.empty(response)) {
return;
}
@@ -111,7 +141,7 @@ const vimeo = {
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// Set and show poster
ui.setPoster.call(player, url.href);
ui.setPoster.call(player, url.href).catch(() => {});
});
// Setup instance
@@ -160,7 +190,7 @@ const vimeo = {
// Set seeking state and trigger event
media.seeking = true;
utils.dispatchEvent.call(player, media, 'seeking');
triggerEvent.call(player, media, 'seeking');
// If paused, mute until seek is complete
Promise.resolve(restorePause && embed.setVolume(0))
@@ -187,7 +217,7 @@ const vimeo = {
.setPlaybackRate(input)
.then(() => {
speed = input;
utils.dispatchEvent.call(player, player.media, 'ratechange');
triggerEvent.call(player, player.media, 'ratechange');
})
.catch(error => {
// Hide menu item (and menu if empty)
@@ -207,7 +237,7 @@ const vimeo = {
set(input) {
player.embed.setVolume(input).then(() => {
volume = input;
utils.dispatchEvent.call(player, player.media, 'volumechange');
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
@@ -219,11 +249,11 @@ const vimeo = {
return muted;
},
set(input) {
const toggle = utils.is.boolean(input) ? input : false;
const toggle = is.boolean(input) ? input : false;
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
muted = toggle;
utils.dispatchEvent.call(player, player.media, 'volumechange');
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
@@ -235,7 +265,7 @@ const vimeo = {
return loop;
},
set(input) {
const toggle = utils.is.boolean(input) ? input : player.config.loop.active;
const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => {
loop = toggle;
@@ -249,6 +279,7 @@ const vimeo = {
.getVideoUrl()
.then(value => {
currentSrc = value;
controls.setDownloadLink.call(player);
})
.catch(error => {
this.debug.warn(error);
@@ -268,12 +299,9 @@ const vimeo = {
});
// Set aspect ratio based on video size
Promise.all([
player.embed.getVideoWidth(),
player.embed.getVideoHeight(),
]).then(dimensions => {
const ratio = utils.getAspectRatio(dimensions[0], dimensions[1]);
vimeo.setAspectRatio.call(this, ratio);
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
vimeo.ratio = getAspectRatio(dimensions[0], dimensions[1]);
vimeo.setAspectRatio.call(this, vimeo.ratio);
});
// Set autopause
@@ -290,13 +318,13 @@ const vimeo = {
// Get current time
player.embed.getCurrentTime().then(value => {
currentTime = value;
utils.dispatchEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'timeupdate');
});
// Get duration
player.embed.getDuration().then(value => {
player.media.duration = value;
utils.dispatchEvent.call(player, player.media, 'durationchange');
triggerEvent.call(player, player.media, 'durationchange');
});
// Get captions
@@ -306,7 +334,7 @@ const vimeo = {
});
player.embed.on('cuechange', ({ cues = [] }) => {
const strippedCues = cues.map(cue => utils.stripHTML(cue.text));
const strippedCues = cues.map(cue => stripHTML(cue.text));
captions.updateCues.call(player, strippedCues);
});
@@ -315,11 +343,11 @@ const vimeo = {
player.embed.getPaused().then(paused => {
assurePlaybackState.call(player, !paused);
if (!paused) {
utils.dispatchEvent.call(player, player.media, 'playing');
triggerEvent.call(player, player.media, 'playing');
}
});
if (utils.is.element(player.embed.element) && player.supported.ui) {
if (is.element(player.embed.element) && player.supported.ui) {
const frame = player.embed.element;
// Fix keyboard focus issues
@@ -330,7 +358,7 @@ const vimeo = {
player.embed.on('play', () => {
assurePlaybackState.call(player, true);
utils.dispatchEvent.call(player, player.media, 'playing');
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('pause', () => {
@@ -340,16 +368,16 @@ const vimeo = {
player.embed.on('timeupdate', data => {
player.media.seeking = false;
currentTime = data.seconds;
utils.dispatchEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'timeupdate');
});
player.embed.on('progress', data => {
player.media.buffered = data.percent;
utils.dispatchEvent.call(player, player.media, 'progress');
triggerEvent.call(player, player.media, 'progress');
// Check all loaded
if (parseInt(data.percent, 10) === 1) {
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
triggerEvent.call(player, player.media, 'canplaythrough');
}
// Get duration as if we do it before load, it gives an incorrect value
@@ -357,24 +385,40 @@ const vimeo = {
player.embed.getDuration().then(value => {
if (value !== player.media.duration) {
player.media.duration = value;
utils.dispatchEvent.call(player, player.media, 'durationchange');
triggerEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('seeked', () => {
player.media.seeking = false;
utils.dispatchEvent.call(player, player.media, 'seeked');
triggerEvent.call(player, player.media, 'seeked');
});
player.embed.on('ended', () => {
player.media.paused = true;
utils.dispatchEvent.call(player, player.media, 'ended');
triggerEvent.call(player, player.media, 'ended');
});
player.embed.on('error', detail => {
player.media.error = detail;
utils.dispatchEvent.call(player, player.media, 'error');
triggerEvent.call(player, player.media, 'error');
});
// Set height/width on fullscreen
player.on('enterfullscreen exitfullscreen', event => {
const { target } = player.fullscreen;
// Ignore for iOS native
if (target !== player.elements.container) {
return;
}
const toggle = event.type === 'enterfullscreen';
const [x, y] = vimeo.ratio.split(':').map(Number);
const dimension = x > y ? 'width' : 'height';
target.style[dimension] = toggle ? `${vimeo.padding}%` : null;
});
// Rebuild UI
+82 -167
View File
@@ -2,66 +2,23 @@
// YouTube plugin
// ==========================================================================
import controls from './../controls';
import ui from './../ui';
import utils from './../utils';
import ui from '../ui';
import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import loadImage from '../utils/loadImage';
import loadScript from '../utils/loadScript';
import { format, generateId } from '../utils/strings';
// Standardise YouTube quality unit
function mapQualityUnit(input) {
switch (input) {
case 'hd2160':
return 2160;
case 2160:
return 'hd2160';
case 'hd1440':
return 1440;
case 1440:
return 'hd1440';
case 'hd1080':
return 1080;
case 1080:
return 'hd1080';
case 'hd720':
return 720;
case 720:
return 'hd720';
case 'large':
return 480;
case 480:
return 'large';
case 'medium':
return 360;
case 360:
return 'medium';
case 'small':
return 240;
case 240:
return 'small';
default:
return 'default';
}
}
function mapQualityUnits(levels) {
if (utils.is.empty(levels)) {
return levels;
// Parse YouTube ID from URL
function parseId(url) {
if (is.empty(url)) {
return null;
}
return utils.dedupe(levels.map(level => mapQualityUnit(level)));
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
return url.match(regex) ? RegExp.$2 : url;
}
// Set playback state and trigger change (only on actual change)
@@ -71,24 +28,24 @@ function assurePlaybackState(play) {
}
if (this.media.paused === play) {
this.media.paused = !play;
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
const youtube = {
setup() {
// Add embed class for responsive
utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set aspect ratio
youtube.setAspectRatio.call(this);
// Setup API
if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) {
if (is.object(window.YT) && is.function(window.YT.Player)) {
youtube.ready.call(this);
} else {
// Load the API
utils.loadScript(this.config.urls.youtube.sdk).catch(error => {
loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
@@ -115,10 +72,10 @@ const youtube = {
// Try via undocumented API method first
// This method disappears now and then though...
// https://github.com/sampotts/plyr/issues/709
if (utils.is.function(this.embed.getVideoData)) {
if (is.function(this.embed.getVideoData)) {
const { title } = this.embed.getVideoData();
if (utils.is.empty(title)) {
if (is.empty(title)) {
this.config.title = title;
ui.setTitle.call(this);
return;
@@ -127,13 +84,12 @@ const youtube = {
// Or via Google API
const key = this.config.keys.google;
if (utils.is.string(key) && !utils.is.empty(key)) {
const url = utils.format(this.config.urls.youtube.api, videoId, key);
if (is.string(key) && !is.empty(key)) {
const url = format(this.config.urls.youtube.api, videoId, key);
utils
.fetch(url)
fetch(url)
.then(result => {
if (utils.is.object(result)) {
if (is.object(result)) {
this.config.title = result.items[0].snippet.title;
ui.setTitle.call(this);
}
@@ -154,7 +110,7 @@ const youtube = {
// Ignore already setup (race condition)
const currentId = player.media.getAttribute('id');
if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) {
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return;
}
@@ -162,30 +118,36 @@ const youtube = {
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (utils.is.empty(source)) {
if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}
// Replace the <iframe> with a <div> due to YouTube API issues
const videoId = utils.parseYouTubeId(source);
const id = utils.generateId(player.provider);
const container = utils.createElement('div', { id });
player.media = utils.replaceElement(container, player.media);
const videoId = parseId(source);
const id = generateId(player.provider);
// Set poster image
// Get poster, if already set
const { poster } = player;
// Replace media element
const container = createElement('div', { id, poster });
player.media = replaceElement(container, player.media);
// Id to poster wrapper
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
.catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
.catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
.then(image => ui.setPoster.call(player, image.src))
.then(posterSrc => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!posterSrc.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
}
});
})
.catch(() => {});
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
@@ -193,6 +155,7 @@ const youtube = {
videoId,
playerVars: {
autoplay: player.config.autoplay ? 1 : 0, // Autoplay
hl: player.config.hl, // iframe interface language
controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported
rel: 0, // No related vids
showinfo: 0, // Hide info
@@ -211,51 +174,23 @@ const youtube = {
},
events: {
onError(event) {
// If we've already fired an error, don't do it again
// YouTube fires onError twice
if (utils.is.object(player.media.error)) {
return;
// YouTube may fire onError twice, so only handle it once
if (!player.media.error) {
const code = event.data;
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
const message =
{
2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
101: 'The owner of the requested video does not allow it to be played in embedded players.',
150: 'The owner of the requested video does not allow it to be played in embedded players.',
}[code] || 'An unknown error occured';
player.media.error = { code, message };
triggerEvent.call(player, player.media, 'error');
}
const detail = {
code: event.data,
};
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
switch (event.data) {
case 2:
detail.message =
'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.';
break;
case 5:
detail.message =
'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.';
break;
case 100:
detail.message =
'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.';
break;
case 101:
case 150:
detail.message = 'The owner of the requested video does not allow it to be played in embedded players.';
break;
default:
detail.message = 'An unknown error occured';
break;
}
player.media.error = detail;
utils.dispatchEvent.call(player, player.media, 'error');
},
onPlaybackQualityChange() {
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
quality: player.media.quality,
});
},
onPlaybackRateChange(event) {
// Get the instance
@@ -264,9 +199,13 @@ const youtube = {
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
utils.dispatchEvent.call(player, player.media, 'ratechange');
triggerEvent.call(player, player.media, 'ratechange');
},
onReady(event) {
// Bail if onReady has already been called. See issue #1108
if (is.function(player.media.play)) {
return;
}
// Get the instance
const instance = event.target;
@@ -298,14 +237,14 @@ const youtube = {
return Number(instance.getCurrentTime());
},
set(time) {
// If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
if (player.paused) {
// If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
if (player.paused && !player.embed.hasPlayed) {
player.embed.mute();
}
// Set seeking state and trigger event
player.media.seeking = true;
utils.dispatchEvent.call(player, player.media, 'seeking');
triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
@@ -322,24 +261,6 @@ const youtube = {
},
});
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
return mapQualityUnit(instance.getPlaybackQuality());
},
set(input) {
const quality = input;
// Set via API
instance.setPlaybackQuality(mapQualityUnit(quality));
// Trigger request event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
quality,
});
},
});
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
@@ -349,7 +270,7 @@ const youtube = {
set(input) {
volume = input;
instance.setVolume(volume * 100);
utils.dispatchEvent.call(player, player.media, 'volumechange');
triggerEvent.call(player, player.media, 'volumechange');
},
});
@@ -360,10 +281,10 @@ const youtube = {
return muted;
},
set(input) {
const toggle = utils.is.boolean(input) ? input : muted;
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
utils.dispatchEvent.call(player, player.media, 'volumechange');
triggerEvent.call(player, player.media, 'volumechange');
},
});
@@ -389,8 +310,8 @@ const youtube = {
player.media.setAttribute('tabindex', -1);
}
utils.dispatchEvent.call(player, player.media, 'timeupdate');
utils.dispatchEvent.call(player, player.media, 'durationchange');
triggerEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'durationchange');
// Reset timer
clearInterval(player.timers.buffering);
@@ -402,7 +323,7 @@ const youtube = {
// Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
utils.dispatchEvent.call(player, player.media, 'progress');
triggerEvent.call(player, player.media, 'progress');
}
// Set last buffer point
@@ -413,7 +334,7 @@ const youtube = {
clearInterval(player.timers.buffering);
// Trigger event
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
triggerEvent.call(player, player.media, 'canplaythrough');
}
}, 200);
@@ -427,15 +348,12 @@ const youtube = {
// Reset timer
clearInterval(player.timers.playing);
const seeked = player.media.seeking && [
1,
2,
].includes(event.data);
const seeked = player.media.seeking && [1, 2].includes(event.data);
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
utils.dispatchEvent.call(player, player.media, 'seeked');
triggerEvent.call(player, player.media, 'seeked');
}
// Handle events
@@ -448,11 +366,11 @@ const youtube = {
switch (event.data) {
case -1:
// Update scrubber
utils.dispatchEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
utils.dispatchEvent.call(player, player.media, 'progress');
triggerEvent.call(player, player.media, 'progress');
break;
@@ -465,7 +383,7 @@ const youtube = {
instance.stopVideo();
instance.playVideo();
} else {
utils.dispatchEvent.call(player, player.media, 'ended');
triggerEvent.call(player, player.media, 'ended');
}
break;
@@ -477,11 +395,11 @@ const youtube = {
} else {
assurePlaybackState.call(player, true);
utils.dispatchEvent.call(player, player.media, 'playing');
triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
utils.dispatchEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
@@ -489,11 +407,8 @@ const youtube = {
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
utils.dispatchEvent.call(player, player.media, 'durationchange');
triggerEvent.call(player, player.media, 'durationchange');
}
// Get quality
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
}
break;
@@ -511,7 +426,7 @@ const youtube = {
break;
}
utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, {
triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},
+192 -174
View File
@@ -1,14 +1,16 @@
// ==========================================================================
// Plyr
// plyr.js v3.3.12
// plyr.js v3.4.7
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import captions from './captions';
import defaults from './config/defaults';
import { pip } from './config/states';
import { getProviderByUrl, providers, types } from './config/types';
import Console from './console';
import controls from './controls';
import defaults from './defaults';
import Fullscreen from './fullscreen';
import Listeners from './listeners';
import media from './media';
@@ -16,9 +18,14 @@ import Ads from './plugins/ads';
import source from './source';
import Storage from './storage';
import support from './support';
import { providers, types } from './types';
import ui from './ui';
import utils from './utils';
import { closest } from './utils/arrays';
import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';
import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
import is from './utils/is';
import loadSprite from './utils/loadSprite';
import { cloneDeep, extend } from './utils/objects';
import { parseUrl } from './utils/urls';
// Private properties
// TODO: Use a WeakMap for private globals
@@ -41,18 +48,18 @@ class Plyr {
this.media = target;
// String selector passed
if (utils.is.string(this.media)) {
if (is.string(this.media)) {
this.media = document.querySelectorAll(this.media);
}
// jQuery, NodeList or Array passed, use first element
if ((window.jQuery && this.media instanceof jQuery) || utils.is.nodeList(this.media) || utils.is.array(this.media)) {
if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) {
// eslint-disable-next-line
this.media = this.media[0];
}
// Set config
this.config = utils.extend(
this.config = extend(
{},
defaults,
Plyr.defaults,
@@ -69,16 +76,17 @@ class Plyr {
// Elements cache
this.elements = {
container: null,
captions: null,
buttons: {},
display: {},
progress: {},
inputs: {},
settings: {
popup: null,
menu: null,
panes: {},
tabs: {},
panels: {},
buttons: {},
},
captions: null,
};
// Captions
@@ -108,7 +116,7 @@ class Plyr {
this.debug.log('Support', support);
// We need an element to setup
if (utils.is.nullOrUndefined(this.media) || !utils.is.element(this.media)) {
if (is.nullOrUndefined(this.media) || !is.element(this.media)) {
this.debug.error('Setup failed: no suitable element passed');
return;
}
@@ -144,7 +152,6 @@ class Plyr {
// Embed properties
let iframe = null;
let url = null;
let params = null;
// Different setup based on type
switch (type) {
@@ -153,10 +160,10 @@ class Plyr {
iframe = this.media.querySelector('iframe');
// <iframe> type
if (utils.is.element(iframe)) {
if (is.element(iframe)) {
// Detect provider
url = iframe.getAttribute('src');
this.provider = utils.getProviderByUrl(url);
url = parseUrl(iframe.getAttribute('src'));
this.provider = getProviderByUrl(url.toString());
// Rework elements
this.elements.container = this.media;
@@ -166,24 +173,21 @@ class Plyr {
this.elements.container.className = '';
// Get attributes from URL and set config
params = utils.getUrlParams(url);
if (!utils.is.empty(params)) {
const truthy = [
'1',
'true',
];
if (url.search.length) {
const truthy = ['1', 'true'];
if (truthy.includes(params.autoplay)) {
if (truthy.includes(url.searchParams.get('autoplay'))) {
this.config.autoplay = true;
}
if (truthy.includes(params.loop)) {
if (truthy.includes(url.searchParams.get('loop'))) {
this.config.loop.active = true;
}
// TODO: replace fullscreen.iosNative with this playsinline config option
// YouTube requires the playsinline in the URL
if (this.isYouTube) {
this.config.playsinline = truthy.includes(params.playsinline);
this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));
this.config.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?
} else {
this.config.playsinline = true;
}
@@ -197,7 +201,7 @@ class Plyr {
}
// Unsupported or missing provider
if (utils.is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
if (is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
this.debug.error('Setup failed: Invalid provider');
return;
}
@@ -219,7 +223,7 @@ class Plyr {
if (this.media.hasAttribute('autoplay')) {
this.config.autoplay = true;
}
if (this.media.hasAttribute('playsinline')) {
if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {
this.config.playsinline = true;
}
if (this.media.hasAttribute('muted')) {
@@ -245,6 +249,8 @@ class Plyr {
return;
}
this.eventListeners = [];
// Create listeners
this.listeners = new Listeners(this);
@@ -255,14 +261,11 @@ class Plyr {
this.media.plyr = this;
// Wrap media
if (!utils.is.element(this.elements.container)) {
this.elements.container = utils.createElement('div');
utils.wrap(this.media, this.elements.container);
if (!is.element(this.elements.container)) {
this.elements.container = createElement('div');
wrap(this.media, this.elements.container);
}
// Allow focus to be captured
this.elements.container.setAttribute('tabindex', 0);
// Add style hook
ui.addStyleHook.call(this);
@@ -271,7 +274,7 @@ class Plyr {
// Listen for events if debugging
if (this.config.debug) {
utils.on(this.elements.container, this.config.events.join(' '), event => {
on.call(this, this.elements.container, this.config.events.join(' '), event => {
this.debug.log(`event: ${event.type}`);
});
}
@@ -292,12 +295,17 @@ class Plyr {
this.fullscreen = new Fullscreen(this);
// Setup ads if provided
this.ads = new Ads(this);
if (this.config.ads.enabled) {
this.ads = new Ads(this);
}
// Autoplay if required
if (this.config.autoplay) {
this.play();
}
// Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
this.lastSeekTime = 0;
}
// ---------------------------------------
@@ -310,18 +318,23 @@ class Plyr {
get isHTML5() {
return Boolean(this.provider === providers.html5);
}
get isEmbed() {
return Boolean(this.isYouTube || this.isVimeo);
}
get isYouTube() {
return Boolean(this.provider === providers.youtube);
}
get isVimeo() {
return Boolean(this.provider === providers.vimeo);
}
get isVideo() {
return Boolean(this.type === types.video);
}
get isAudio() {
return Boolean(this.type === types.audio);
}
@@ -330,7 +343,7 @@ class Plyr {
* Play the media, or play the advertisement (if they are not blocked)
*/
play() {
if (!utils.is.function(this.media.play)) {
if (!is.function(this.media.play)) {
return null;
}
@@ -342,7 +355,7 @@ class Plyr {
* Pause the media
*/
pause() {
if (!this.playing || !utils.is.function(this.media.pause)) {
if (!this.playing || !is.function(this.media.pause)) {
return;
}
@@ -383,7 +396,7 @@ class Plyr {
*/
togglePlay(input) {
// Toggle based on current state if nothing passed
const toggle = utils.is.boolean(input) ? input : !this.playing;
const toggle = is.boolean(input) ? input : !this.playing;
if (toggle) {
this.play();
@@ -399,7 +412,7 @@ class Plyr {
if (this.isHTML5) {
this.pause();
this.restart();
} else if (utils.is.function(this.media.stop)) {
} else if (is.function(this.media.stop)) {
this.media.stop();
}
}
@@ -416,7 +429,7 @@ class Plyr {
* @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
*/
rewind(seekTime) {
this.currentTime = this.currentTime - (utils.is.number(seekTime) ? seekTime : this.config.seekTime);
this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime);
}
/**
@@ -424,7 +437,7 @@ class Plyr {
* @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
*/
forward(seekTime) {
this.currentTime = this.currentTime + (utils.is.number(seekTime) ? seekTime : this.config.seekTime);
this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime);
}
/**
@@ -438,7 +451,7 @@ class Plyr {
}
// Validate input
const inputIsValid = utils.is.number(input) && input > 0;
const inputIsValid = is.number(input) && input > 0;
// Set
this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
@@ -461,7 +474,7 @@ class Plyr {
const { buffered } = this.media;
// YouTube / Vimeo return a float between 0-1
if (utils.is.number(buffered)) {
if (is.number(buffered)) {
return buffered;
}
@@ -489,8 +502,9 @@ class Plyr {
// Faux duration set via config
const fauxDuration = parseFloat(this.config.duration);
// Media duration can be NaN before the media has loaded
const duration = (this.media || {}).duration || 0;
// 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;
// If config duration is funky, use regular duration
return fauxDuration || duration;
@@ -505,17 +519,17 @@ class Plyr {
const max = 1;
const min = 0;
if (utils.is.string(volume)) {
if (is.string(volume)) {
volume = Number(volume);
}
// Load volume from storage if no value specified
if (!utils.is.number(volume)) {
if (!is.number(volume)) {
volume = this.storage.get('volume');
}
// Use config if all else fails
if (!utils.is.number(volume)) {
if (!is.number(volume)) {
({ volume } = this.config);
}
@@ -535,7 +549,7 @@ class Plyr {
this.media.volume = volume;
// If muted, and we're increasing volume manually, reset muted state
if (!utils.is.empty(value) && this.muted && volume > 0) {
if (!is.empty(value) && this.muted && volume > 0) {
this.muted = false;
}
}
@@ -553,7 +567,7 @@ class Plyr {
*/
increaseVolume(step) {
const volume = this.media.muted ? 0 : this.volume;
this.volume = volume + (utils.is.number(step) ? step : 1);
this.volume = volume + (is.number(step) ? step : 0);
}
/**
@@ -561,8 +575,7 @@ class Plyr {
* @param {boolean} step - How much to decrease by (between 0 and 1)
*/
decreaseVolume(step) {
const volume = this.media.muted ? 0 : this.volume;
this.volume = volume - (utils.is.number(step) ? step : 1);
this.increaseVolume(-step);
}
/**
@@ -573,12 +586,12 @@ class Plyr {
let toggle = mute;
// Load muted state from storage
if (!utils.is.boolean(toggle)) {
if (!is.boolean(toggle)) {
toggle = this.storage.get('muted');
}
// Use config if all else fails
if (!utils.is.boolean(toggle)) {
if (!is.boolean(toggle)) {
toggle = this.config.muted;
}
@@ -624,15 +637,15 @@ class Plyr {
set speed(input) {
let speed = null;
if (utils.is.number(input)) {
if (is.number(input)) {
speed = input;
}
if (!utils.is.number(speed)) {
if (!is.number(speed)) {
speed = this.storage.get('speed');
}
if (!utils.is.number(speed)) {
if (!is.number(speed)) {
speed = this.config.speed.selected;
}
@@ -669,39 +682,41 @@ class Plyr {
* @param {number} input - Quality level
*/
set quality(input) {
let quality = null;
const config = this.config.quality;
const options = this.options.quality;
if (!utils.is.empty(input)) {
quality = Number(input);
}
if (!utils.is.number(quality)) {
quality = this.storage.get('quality');
}
if (!utils.is.number(quality)) {
quality = this.config.quality.selected;
}
if (!utils.is.number(quality)) {
quality = this.config.quality.default;
}
if (!this.options.quality.length) {
if (!options.length) {
return;
}
if (!this.options.quality.includes(quality)) {
const closest = utils.closest(this.options.quality, quality);
this.debug.warn(`Unsupported quality option: ${quality}, using ${closest} instead`);
quality = closest;
let quality = [
!is.empty(input) && Number(input),
this.storage.get('quality'),
config.selected,
config.default,
].find(is.number);
let updateStorage = true;
if (!options.includes(quality)) {
const value = closest(options, quality);
this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);
quality = value;
// Don't update storage if quality is not supported
updateStorage = false;
}
// Update config
this.config.quality.selected = quality;
config.selected = quality;
// Set quality
this.media.quality = quality;
// Save to storage
if (updateStorage) {
this.storage.set({ quality });
}
}
/**
@@ -717,7 +732,7 @@ class Plyr {
* @param {boolean} input - Whether to loop or not
*/
set loop(input) {
const toggle = utils.is.boolean(input) ? input : this.config.loop.active;
const toggle = is.boolean(input) ? input : this.config.loop.active;
this.config.loop.active = toggle;
this.media.loop = toggle;
@@ -787,6 +802,15 @@ class Plyr {
return this.media.currentSrc;
}
/**
* Get a download URL (either source or custom)
*/
get download() {
const { download } = this.config.urls;
return is.url(download) ? download : this.source;
}
/**
* Set the poster image for a video
* @param {input} - the URL for the new poster image
@@ -797,7 +821,7 @@ class Plyr {
return;
}
ui.setPoster.call(this, input);
ui.setPoster.call(this, input, false).catch(() => {});
}
/**
@@ -816,7 +840,7 @@ class Plyr {
* @param {boolean} input - Whether to autoplay or not
*/
set autoplay(input) {
const toggle = utils.is.boolean(input) ? input : this.config.autoplay;
const toggle = is.boolean(input) ? input : this.config.autoplay;
this.config.autoplay = toggle;
}
@@ -832,25 +856,7 @@ class Plyr {
* @param {boolean} input - Whether to enable captions
*/
toggleCaptions(input) {
// If there's no full support
if (!this.supported.ui) {
return;
}
// If the method is called without parameter, toggle based on current value
const active = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
// Toggle state
utils.toggleState(this.elements.buttons.captions, active);
// Add class hook
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, active);
// Update state and trigger event
if (active !== this.captions.active) {
this.captions.active = active;
utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
}
captions.toggle.call(this, input, false);
}
/**
@@ -858,15 +864,15 @@ class Plyr {
* @param {number} - Caption index
*/
set currentTrack(input) {
captions.set.call(this, input);
captions.set.call(this, input, false);
}
/**
* Get the current caption track index (-1 if disabled)
*/
get currentTrack() {
const { active, currentTrack } = this.captions;
return active ? currentTrack : -1;
const { toggled, currentTrack } = this.captions;
return toggled ? currentTrack : -1;
}
/**
@@ -875,7 +881,7 @@ class Plyr {
* @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
*/
set language(input) {
captions.setLanguage.call(this, input);
captions.setLanguage.call(this, input, false);
}
/**
@@ -891,21 +897,28 @@ class Plyr {
* TODO: detect outside changes
*/
set pip(input) {
const states = {
pip: 'picture-in-picture',
inline: 'inline',
};
// Bail if no support
if (!support.pip) {
return;
}
// Toggle based on current state if not passed
const toggle = utils.is.boolean(input) ? input : this.pip === states.inline;
const toggle = is.boolean(input) ? input : !this.pip;
// Toggle based on current state
this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline);
// Safari
if (is.function(this.media.webkitSetPresentationMode)) {
this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);
}
// Chrome
if (is.function(this.media.requestPictureInPicture)) {
if (!this.pip && toggle) {
this.media.requestPictureInPicture();
} else if (this.pip && !toggle) {
document.exitPictureInPicture();
}
}
}
/**
@@ -916,7 +929,13 @@ class Plyr {
return null;
}
return this.media.webkitPresentationMode;
// Safari
if (!is.empty(this.media.webkitPresentationMode)) {
return this.media.webkitPresentationMode === pip.active;
}
// Chrome
return this.media === document.pictureInPictureElement;
}
/**
@@ -938,25 +957,28 @@ class Plyr {
// Don't toggle if missing UI support or if it's audio
if (this.supported.ui && !this.isAudio) {
// Get state before change
const isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);
// Negate the argument if not undefined since adding the class to hides the controls
const force = typeof toggle === 'undefined' ? undefined : !toggle;
// Apply and get updated state
const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force);
const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);
// Close menu
if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
controls.toggleMenu.call(this, false);
}
// Trigger event on change
if (hiding !== isHidden) {
const eventName = hiding ? 'controlshidden' : 'controlsshown';
utils.dispatchEvent.call(this, this.media, eventName);
triggerEvent.call(this, this.media, eventName);
}
return !hiding;
}
return false;
}
@@ -966,7 +988,16 @@ class Plyr {
* @param {function} callback - Callback for when event occurs
*/
on(event, callback) {
utils.on(this.elements.container, event, callback);
on.call(this, this.elements.container, event, callback);
}
/**
* Add event listeners once
* @param {string} event - Event type
* @param {function} callback - Callback for when event occurs
*/
once(event, callback) {
once.call(this, this.elements.container, event, callback);
}
/**
@@ -975,7 +1006,7 @@ class Plyr {
* @param {function} callback - Callback for when event occurs
*/
off(event, callback) {
utils.off(this.elements.container, event, callback);
off(this.elements.container, event, callback);
}
/**
@@ -1001,10 +1032,10 @@ class Plyr {
if (soft) {
if (Object.keys(this.elements).length) {
// Remove elements
utils.removeElement(this.elements.buttons.play);
utils.removeElement(this.elements.captions);
utils.removeElement(this.elements.controls);
utils.removeElement(this.elements.wrapper);
removeElement(this.elements.buttons.play);
removeElement(this.elements.captions);
removeElement(this.elements.controls);
removeElement(this.elements.wrapper);
// Clear for GC
this.elements.buttons.play = null;
@@ -1014,21 +1045,21 @@ class Plyr {
}
// Callback
if (utils.is.function(callback)) {
if (is.function(callback)) {
callback();
}
} else {
// Unbind listeners
this.listeners.clear();
unbindListeners.call(this);
// Replace the container with the original element provided
utils.replaceElement(this.elements.original, this.elements.container);
replaceElement(this.elements.original, this.elements.container);
// Event
utils.dispatchEvent.call(this, this.elements.original, 'destroyed', true);
triggerEvent.call(this, this.elements.original, 'destroyed', true);
// Callback
if (utils.is.function(callback)) {
if (is.function(callback)) {
callback.call(this.elements.original);
}
@@ -1046,50 +1077,37 @@ class Plyr {
// Stop playback
this.stop();
// Type specific stuff
switch (`${this.provider}:${this.type}`) {
case 'html5:video':
case 'html5:audio':
// Clear timeout
clearTimeout(this.timers.loading);
// Provider specific stuff
if (this.isHTML5) {
// Clear timeout
clearTimeout(this.timers.loading);
// Restore native video controls
ui.toggleNativeControls.call(this, true);
// Restore native video controls
ui.toggleNativeControls.call(this, true);
// Clean up
done();
// Clean up
done();
} else if (this.isYouTube) {
// Clear timers
clearInterval(this.timers.buffering);
clearInterval(this.timers.playing);
break;
// Destroy YouTube API
if (this.embed !== null && is.function(this.embed.destroy)) {
this.embed.destroy();
}
case 'youtube:video':
// Clear timers
clearInterval(this.timers.buffering);
clearInterval(this.timers.playing);
// Clean up
done();
} else if (this.isVimeo) {
// Destroy Vimeo API
// then clean up (wait, to prevent postmessage errors)
if (this.embed !== null) {
this.embed.unload().then(done);
}
// Destroy YouTube API
if (this.embed !== null && utils.is.function(this.embed.destroy)) {
this.embed.destroy();
}
// Clean up
done();
break;
case 'vimeo:video':
// Destroy Vimeo API
// then clean up (wait, to prevent postmessage errors)
if (this.embed !== null) {
this.embed.unload().then(done);
}
// Vimeo does not always return
setTimeout(done, 200);
break;
default:
break;
// Vimeo does not always return
setTimeout(done, 200);
}
}
@@ -1117,7 +1135,7 @@ class Plyr {
* @param {string} [id] - Unique ID
*/
static loadSprite(url, id) {
return utils.loadSprite(url, id);
return loadSprite(url, id);
}
/**
@@ -1128,15 +1146,15 @@ class Plyr {
static setup(selector, options = {}) {
let targets = null;
if (utils.is.string(selector)) {
if (is.string(selector)) {
targets = Array.from(document.querySelectorAll(selector));
} else if (utils.is.nodeList(selector)) {
} else if (is.nodeList(selector)) {
targets = Array.from(selector);
} else if (utils.is.array(selector)) {
targets = selector.filter(utils.is.element);
} else if (is.array(selector)) {
targets = selector.filter(is.element);
}
if (utils.is.empty(targets)) {
if (is.empty(targets)) {
return null;
}
@@ -1144,6 +1162,6 @@ class Plyr {
}
}
Plyr.defaults = utils.cloneDeep(defaults);
Plyr.defaults = cloneDeep(defaults);
export default Plyr;
+1 -2
View File
@@ -1,11 +1,10 @@
// ==========================================================================
// Plyr Polyfilled Build
// plyr.js v3.3.12
// plyr.js v3.4.7
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import 'babel-polyfill';
import 'custom-event-polyfill';
import 'url-polyfill';
import Plyr from './plyr';
+32 -41
View File
@@ -2,23 +2,25 @@
// Plyr source update
// ==========================================================================
import { providers } from './config/types';
import html5 from './html5';
import media from './media';
import support from './support';
import { providers } from './types';
import ui from './ui';
import utils from './utils';
import { createElement, insertElement, removeElement } from './utils/elements';
import is from './utils/is';
import { getDeep } from './utils/objects';
const source = {
// Add elements to HTML5 media (source, tracks, etc)
insertElements(type, attributes) {
if (utils.is.string(attributes)) {
utils.insertElement(type, this.media, {
if (is.string(attributes)) {
insertElement(type, this.media, {
src: attributes,
});
} else if (utils.is.array(attributes)) {
} else if (is.array(attributes)) {
attributes.forEach(attribute => {
utils.insertElement(type, this.media, attribute);
insertElement(type, this.media, attribute);
});
}
},
@@ -26,7 +28,7 @@ const source = {
// Update source
// Sources are not checked for support so be careful
change(input) {
if (!utils.is.object(input) || !('sources' in input) || !input.sources.length) {
if (!getDeep(input, 'sources.length')) {
this.debug.warn('Invalid source format');
return;
}
@@ -42,47 +44,34 @@ const source = {
this.options.quality = [];
// Remove elements
utils.removeElement(this.media);
removeElement(this.media);
this.media = null;
// Reset class name
if (utils.is.element(this.elements.container)) {
if (is.element(this.elements.container)) {
this.elements.container.removeAttribute('class');
}
// Set the type and provider
this.type = input.type;
this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5;
const { sources, type } = input;
const [{ provider = providers.html5, src }] = sources;
const tagName = provider === 'html5' ? type : 'div';
const attributes = provider === 'html5' ? {} : { src };
// Check for support
this.supported = support.check(this.type, this.provider, this.config.playsinline);
// Create new markup
switch (`${this.provider}:${this.type}`) {
case 'html5:video':
this.media = utils.createElement('video');
break;
case 'html5:audio':
this.media = utils.createElement('audio');
break;
case 'youtube:video':
case 'vimeo:video':
this.media = utils.createElement('div', {
src: input.sources[0].src,
});
break;
default:
break;
}
Object.assign(this, {
provider,
type,
// Check for support
supported: support.check(type, provider, this.config.playsinline),
// Create new element
media: createElement(tagName, attributes),
});
// Inject the new element
this.elements.container.appendChild(this.media);
// Autoplay the new source?
if (utils.is.boolean(input.autoplay)) {
if (is.boolean(input.autoplay)) {
this.config.autoplay = input.autoplay;
}
@@ -94,7 +83,7 @@ const source = {
if (this.config.autoplay) {
this.media.setAttribute('autoplay', '');
}
if (!utils.is.empty(input.poster)) {
if (!is.empty(input.poster)) {
this.poster = input.poster;
}
if (this.config.loop.active) {
@@ -113,7 +102,7 @@ const source = {
// Set new sources for html5
if (this.isHTML5) {
source.insertElements.call(this, 'source', input.sources);
source.insertElements.call(this, 'source', sources);
}
// Set video title
@@ -125,12 +114,9 @@ const source = {
// HTML5 stuff
if (this.isHTML5) {
// Setup captions
if ('tracks' in input) {
if (Object.keys(input).includes('tracks')) {
source.insertElements.call(this, 'track', input.tracks);
}
// Load HTML5 sources
this.media.load();
}
// If HTML5 or embed but not fully supported, setupInterface and call ready now
@@ -139,6 +125,11 @@ const source = {
ui.build.call(this);
}
if (this.isHTML5) {
// Load HTML5 sources
this.media.load();
}
// Update the fullscreen support
this.fullscreen.update();
},
+7 -6
View File
@@ -2,7 +2,8 @@
// Plyr storage
// ==========================================================================
import utils from './utils';
import is from './utils/is';
import { extend } from './utils/objects';
class Storage {
constructor(player) {
@@ -37,13 +38,13 @@ class Storage {
const store = window.localStorage.getItem(this.key);
if (utils.is.empty(store)) {
if (is.empty(store)) {
return null;
}
const json = JSON.parse(store);
return utils.is.string(key) && key.length ? json[key] : json;
return is.string(key) && key.length ? json[key] : json;
}
set(object) {
@@ -53,7 +54,7 @@ class Storage {
}
// Can only store objectst
if (!utils.is.object(object)) {
if (!is.object(object)) {
return;
}
@@ -61,12 +62,12 @@ class Storage {
let storage = this.get();
// Default to empty object
if (utils.is.empty(storage)) {
if (is.empty(storage)) {
storage = {};
}
// Update the working copy of the values
utils.extend(storage, object);
extend(storage, object);
// Update storage
window.localStorage.setItem(this.key, JSON.stringify(storage));
+50 -99
View File
@@ -2,7 +2,19 @@
// Plyr support checks
// ==========================================================================
import utils from './utils';
import { transitionEndEvent } from './utils/animation';
import browser from './utils/browser';
import { createElement } from './utils/elements';
import is from './utils/is';
// Default codecs for checking mimetype support
const defaultCodecs = {
'audio/ogg': 'vorbis',
'audio/wav': '1',
'video/webm': 'vp8, vorbis',
'video/mp4': 'avc1.42E01E, mp4a.40.2',
'video/ogg': 'theora',
};
// Check for feature support
const support = {
@@ -13,32 +25,9 @@ const support = {
// Check for support
// Basic functionality vs full UI
check(type, provider, playsinline) {
let api = false;
let ui = false;
const browser = utils.getBrowser();
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
switch (`${provider}:${type}`) {
case 'html5:video':
api = support.video;
ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline);
break;
case 'html5:audio':
api = support.audio;
ui = api && support.rangeInput;
break;
case 'youtube:video':
case 'vimeo:video':
api = true;
ui = support.rangeInput && (!browser.isIPhone || canPlayInline);
break;
default:
api = support.audio && support.video;
ui = api && support.rangeInput;
}
const api = support[type] || provider !== 'html5';
const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
return {
api,
@@ -47,15 +36,30 @@ const support = {
},
// Picture-in-picture support
// Safari only currently
// Safari & Chrome only currently
pip: (() => {
const browser = utils.getBrowser();
return !browser.isIPhone && utils.is.function(utils.createElement('video').webkitSetPresentationMode);
if (browser.isIPhone) {
return false;
}
// Safari
// https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls
if (is.function(createElement('video').webkitSetPresentationMode)) {
return true;
}
// Chrome
// https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture
if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {
return true;
}
return false;
})(),
// Airplay support
// Safari only currently
airplay: utils.is.function(window.WebKitPlaybackTargetAvailabilityEvent),
airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
// Inline playback support
// https://webkit.org/blog/6784/new-video-policies-for-ios/
@@ -64,83 +68,30 @@ const support = {
// Check for mime type support against a player instance
// Credits: http://diveintohtml5.info/everything.html
// Related: http://www.leanbackplayer.com/test/h5mt.html
mime(type) {
const { media } = this;
mime(inputType) {
const [mediaType] = inputType.split('/');
let type = inputType;
try {
// Bail if no checking function
if (!this.isHTML5 || !utils.is.function(media.canPlayType)) {
return false;
}
// Check directly if codecs specified
if (type.includes('codecs=')) {
return media.canPlayType(type).replace(/no/, '');
}
// Type specific checks
if (this.isVideo) {
switch (type) {
case 'video/webm':
return media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, '');
case 'video/mp4':
return media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, '');
case 'video/ogg':
return media.canPlayType('video/ogg; codecs="theora"').replace(/no/, '');
default:
return false;
}
} else if (this.isAudio) {
switch (type) {
case 'audio/mpeg':
return media.canPlayType('audio/mpeg;').replace(/no/, '');
case 'audio/ogg':
return media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, '');
case 'audio/wav':
return media.canPlayType('audio/wav; codecs="1"').replace(/no/, '');
default:
return false;
}
}
} catch (e) {
// Verify we're using HTML5 and there's no media type mismatch
if (!this.isHTML5 || mediaType !== this.type) {
return false;
}
// If we got this far, we're stuffed
return false;
// Add codec if required
if (Object.keys(defaultCodecs).includes(type)) {
type += `; codecs="${defaultCodecs[inputType]}"`;
}
try {
return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
} catch (e) {
return false;
}
},
// Check for textTracks support
textTracks: 'textTracks' in document.createElement('video'),
// Check for passive event listener support
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
// https://www.youtube.com/watch?v=NPM6172J22g
passiveListeners: (() => {
// Test via a getter in the options object to see if the passive property is accessed
let supported = false;
try {
const options = Object.defineProperty({}, 'passive', {
get() {
supported = true;
return null;
},
});
window.addEventListener('test', null, options);
window.removeEventListener('test', null, options);
} catch (e) {
// Do nothing
}
return supported;
})(),
// <input type="range"> Sliders
rangeInput: (() => {
const range = document.createElement('input');
@@ -153,7 +104,7 @@ const support = {
touch: 'ontouchstart' in document.documentElement,
// Detect transitions support
transitions: utils.transitionEndEvent !== false,
transitions: transitionEndEvent !== false,
// Reduced motion iOS & MacOS setting
// https://webkit.org/blog/7551/responsive-design-for-motion/
+80 -65
View File
@@ -4,17 +4,18 @@
import captions from './captions';
import controls from './controls';
import i18n from './i18n';
import support from './support';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
import browser from './utils/browser';
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';
const ui = {
addStyleHook() {
utils.toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
utils.toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
},
// Toggle native HTML5 media controls
@@ -44,7 +45,7 @@ const ui = {
}
// Inject custom controls if not present
if (!utils.is.element(this.elements.controls)) {
if (!is.element(this.elements.controls)) {
// Inject custom controls
controls.inject.call(this);
@@ -85,31 +86,35 @@ const ui = {
ui.checkPlaying.call(this);
// Check for picture-in-picture support
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
toggleClass(
this.elements.container,
this.config.classNames.pip.supported,
support.pip && this.isHTML5 && this.isVideo,
);
// Check for airplay support
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
// Ready for API calls
this.ready = true;
// Ready event at end of execution stack
setTimeout(() => {
utils.dispatchEvent.call(this, this.media, 'ready');
triggerEvent.call(this, this.media, 'ready');
}, 0);
// Set the title
ui.setTitle.call(this);
// Assure the poster image is set, if the property was added before the element was created
if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) {
ui.setPoster.call(this, this.poster);
if (this.poster) {
ui.setPoster.call(this, this.poster, false).catch(() => {});
}
// Manually set the duration if user has overridden it.
@@ -125,31 +130,26 @@ const ui = {
let label = i18n.get('play', this.config);
// If there's a media title set, use that for the label
if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) {
if (is.string(this.config.title) && !is.empty(this.config.title)) {
label += `, ${this.config.title}`;
// Set container label
this.elements.container.setAttribute('aria-label', this.config.title);
}
// If there's a play button, set label
if (utils.is.nodeList(this.elements.buttons.play)) {
Array.from(this.elements.buttons.play).forEach(button => {
button.setAttribute('aria-label', label);
});
}
Array.from(this.elements.buttons.play || []).forEach(button => {
button.setAttribute('aria-label', label);
});
// Set iframe title
// https://github.com/sampotts/plyr/issues/124
if (this.isEmbed) {
const iframe = utils.getElement.call(this, 'iframe');
const iframe = getElement.call(this, 'iframe');
if (!utils.is.element(iframe)) {
if (!is.element(iframe)) {
return;
}
// Default to media type
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
const title = !is.empty(this.config.title) ? this.config.title : 'video';
const format = i18n.get('frameTitle', this.config);
iframe.setAttribute('title', format.replace('{title}', title));
@@ -158,51 +158,66 @@ const ui = {
// Toggle poster
togglePoster(enable) {
utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
},
// Set the poster image (async)
setPoster(poster) {
// Set property regardless of validity
this.media.setAttribute('poster', poster);
// Bail if element is missing
if (!utils.is.element(this.elements.poster)) {
return Promise.reject();
// Used internally for the poster setter, with the passive option forced to false
setPoster(poster, passive = true) {
// Don't override if call is passive
if (passive && this.poster) {
return Promise.reject(new Error('Poster already set'));
}
// Load the image, and set poster if successful
const loadPromise = utils.loadImage(poster)
.then(() => {
this.elements.poster.style.backgroundImage = `url('${poster}')`;
Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`,
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
ui.togglePoster.call(this, true);
return poster;
});
// Set property synchronously to respect the call order
this.media.setAttribute('poster', poster);
// Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video)
loadPromise.catch(() => ui.togglePoster.call(this, false));
// Return the promise so the caller can use it as well
return loadPromise;
// Wait until ui is ready
return (
ready
.call(this)
// Load image
.then(() => loadImage(poster))
.catch(err => {
// Hide poster on error unless it's been set by another call
if (poster === this.poster) {
ui.togglePoster.call(this, false);
}
// Rethrow
throw err;
})
.then(() => {
// Prevent race conditions
if (poster !== this.poster) {
throw new Error('setPoster cancelled by later call to setPoster');
}
})
.then(() => {
Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`,
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
ui.togglePoster.call(this, true);
return poster;
})
);
},
// Check playing state
checkPlaying(event) {
// Class hooks
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set ARIA state
utils.toggleState(this.elements.buttons.play, this.playing);
// Set state
Array.from(this.elements.buttons.play || []).forEach(target => {
target.pressed = this.playing;
});
// Only update controls on non timeupdate events
if (utils.is.event(event) && event.type === 'timeupdate') {
if (is.event(event) && event.type === 'timeupdate') {
return;
}
@@ -212,10 +227,7 @@ const ui = {
// Check if media is loading
checkLoading(event) {
this.loading = [
'stalled',
'waiting',
].includes(event.type);
this.loading = ['stalled', 'waiting'].includes(event.type);
// Clear timer
clearTimeout(this.timers.loading);
@@ -223,7 +235,7 @@ const ui = {
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => {
// Update progress bar loading class state
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Update controls visibility
ui.toggleControls.call(this);
@@ -235,8 +247,11 @@ const ui = {
const { controls } = this.elements;
if (controls && this.config.hideControls) {
// Show controls if force, loading, paused, or button interaction, otherwise hide
this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover));
// Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
const recentTouchSeek = (this.touch && this.lastSeekTime + 2000 > Date.now());
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover || recentTouchSeek));
}
},
};
-875
View File
@@ -1,875 +0,0 @@
// ==========================================================================
// Plyr utils
// ==========================================================================
import loadjs from 'loadjs';
import Storage from './storage';
import support from './support';
import { providers } from './types';
const utils = {
// Check variable types
is: {
object(input) {
return utils.getConstructor(input) === Object;
},
number(input) {
return utils.getConstructor(input) === Number && !Number.isNaN(input);
},
string(input) {
return utils.getConstructor(input) === String;
},
boolean(input) {
return utils.getConstructor(input) === Boolean;
},
function(input) {
return utils.getConstructor(input) === Function;
},
array(input) {
return !utils.is.nullOrUndefined(input) && Array.isArray(input);
},
weakMap(input) {
return utils.is.instanceof(input, WeakMap);
},
nodeList(input) {
return utils.is.instanceof(input, NodeList);
},
element(input) {
return utils.is.instanceof(input, Element);
},
textNode(input) {
return utils.getConstructor(input) === Text;
},
event(input) {
return utils.is.instanceof(input, Event);
},
cue(input) {
return utils.is.instanceof(input, window.TextTrackCue) || utils.is.instanceof(input, window.VTTCue);
},
track(input) {
return utils.is.instanceof(input, TextTrack) || (!utils.is.nullOrUndefined(input) && utils.is.string(input.kind));
},
url(input) {
return !utils.is.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input);
},
nullOrUndefined(input) {
return input === null || typeof input === 'undefined';
},
empty(input) {
return (
utils.is.nullOrUndefined(input) ||
((utils.is.string(input) || utils.is.array(input) || utils.is.nodeList(input)) && !input.length) ||
(utils.is.object(input) && !Object.keys(input).length)
);
},
instanceof(input, constructor) {
return Boolean(input && constructor && input instanceof constructor);
},
},
getConstructor(input) {
return !utils.is.nullOrUndefined(input) ? input.constructor : null;
},
// Unfortunately, due to mixed support, UA sniffing is required
getBrowser() {
return {
isIE: /* @cc_on!@ */ false || !!document.documentMode,
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform),
};
},
// Fetch wrapper
// Using XHR to avoid issues with older browsers
fetch(url, responseType = 'text') {
return new Promise((resolve, reject) => {
try {
const request = new XMLHttpRequest();
// Check for CORS support
if (!('withCredentials' in request)) {
return;
}
request.addEventListener('load', () => {
if (responseType === 'text') {
try {
resolve(JSON.parse(request.responseText));
} catch (e) {
resolve(request.responseText);
}
} else {
resolve(request.response);
}
});
request.addEventListener('error', () => {
throw new Error(request.statusText);
});
request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send();
} catch (e) {
reject(e);
}
});
},
// Load image avoiding xhr/fetch CORS issues
// Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded.
// By default it checks if it is at least 1px, but you can add a second argument to change this.
loadImage(src, minWidth = 1) {
return new Promise((resolve, reject) => {
const image = new Image();
const handler = () => {
delete image.onload;
delete image.onerror;
(image.naturalWidth >= minWidth ? resolve : reject)(image);
};
Object.assign(image, {onload: handler, onerror: handler, src});
});
},
// Load an external script
loadScript(url) {
return new Promise((resolve, reject) => {
loadjs(url, {
success: resolve,
error: reject,
});
});
},
// Load an external SVG sprite
loadSprite(url, id) {
if (!utils.is.string(url)) {
return;
}
const prefix = 'cache';
const hasId = utils.is.string(id);
let isCached = false;
const exists = () => document.getElementById(id) !== null;
const update = (container, data) => {
container.innerHTML = data;
// Check again incase of race condition
if (hasId && exists()) {
return;
}
// Inject the SVG to the body
document.body.insertAdjacentElement('afterbegin', container);
};
// Only load once if ID set
if (!hasId || !exists()) {
const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
utils.toggleHidden(container, true);
if (hasId) {
container.setAttribute('id', id);
}
// Check in cache
if (useStorage) {
const cached = window.localStorage.getItem(`${prefix}-${id}`);
isCached = cached !== null;
if (isCached) {
const data = JSON.parse(cached);
update(container, data.content);
}
}
// Get the sprite
utils
.fetch(url)
.then(result => {
if (utils.is.empty(result)) {
return;
}
if (useStorage) {
window.localStorage.setItem(
`${prefix}-${id}`,
JSON.stringify({
content: result,
}),
);
}
update(container, result);
})
.catch(() => {});
}
},
// Generate a random ID
generateId(prefix) {
return `${prefix}-${Math.floor(Math.random() * 10000)}`;
},
// Wrap an element
wrap(elements, wrapper) {
// Convert `elements` to an array, if necessary.
const targets = elements.length ? elements : [elements];
// Loops backwards to prevent having to clone the wrapper on the
// first element (see `child` below).
Array.from(targets)
.reverse()
.forEach((element, index) => {
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
// Cache the current parent and sibling.
const parent = element.parentNode;
const sibling = element.nextSibling;
// Wrap the element (is automatically removed from its current
// parent).
child.appendChild(element);
// If the element had a sibling, insert the wrapper before
// the sibling to maintain the HTML structure; otherwise, just
// append it to the parent.
if (sibling) {
parent.insertBefore(child, sibling);
} else {
parent.appendChild(child);
}
});
},
// Create a DocumentFragment
createElement(type, attributes, text) {
// Create a new <element>
const element = document.createElement(type);
// Set all passed attributes
if (utils.is.object(attributes)) {
utils.setAttributes(element, attributes);
}
// Add text node
if (utils.is.string(text)) {
element.innerText = text;
}
// Return built element
return element;
},
// Inaert an element after another
insertAfter(element, target) {
target.parentNode.insertBefore(element, target.nextSibling);
},
// Insert a DocumentFragment
insertElement(type, parent, attributes, text) {
// Inject the new <element>
parent.appendChild(utils.createElement(type, attributes, text));
},
// Remove element(s)
removeElement(element) {
if (utils.is.nodeList(element) || utils.is.array(element)) {
Array.from(element).forEach(utils.removeElement);
return;
}
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
return;
}
element.parentNode.removeChild(element);
},
// Remove all child elements
emptyElement(element) {
let { length } = element.childNodes;
while (length > 0) {
element.removeChild(element.lastChild);
length -= 1;
}
},
// Replace element
replaceElement(newChild, oldChild) {
if (!utils.is.element(oldChild) || !utils.is.element(oldChild.parentNode) || !utils.is.element(newChild)) {
return null;
}
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
},
// Set attributes
setAttributes(element, attributes) {
if (!utils.is.element(element) || utils.is.empty(attributes)) {
return;
}
Object.entries(attributes).forEach(([
key,
value,
]) => {
element.setAttribute(key, value);
});
},
// Get an attribute object from a string selector
getAttributesFromSelector(sel, existingAttributes) {
// For example:
// '.test' to { class: 'test' }
// '#test' to { id: 'test' }
// '[data-test="test"]' to { 'data-test': 'test' }
if (!utils.is.string(sel) || utils.is.empty(sel)) {
return {};
}
const attributes = {};
const existing = existingAttributes;
sel.split(',').forEach(s => {
// Remove whitespace
const selector = s.trim();
const className = selector.replace('.', '');
const stripped = selector.replace(/[[\]]/g, '');
// Get the parts and value
const parts = stripped.split('=');
const key = parts[0];
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
// Get the first character
const start = selector.charAt(0);
switch (start) {
case '.':
// Add to existing classname
if (utils.is.object(existing) && utils.is.string(existing.class)) {
existing.class += ` ${className}`;
}
attributes.class = className;
break;
case '#':
// ID selector
attributes.id = selector.replace('#', '');
break;
case '[':
// Attribute selector
attributes[key] = value;
break;
default:
break;
}
});
return attributes;
},
// Toggle hidden
toggleHidden(element, hidden) {
if (!utils.is.element(element)) {
return;
}
let hide = hidden;
if (!utils.is.boolean(hide)) {
hide = !element.hasAttribute('hidden');
}
if (hide) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
},
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
toggleClass(element, className, force) {
if (utils.is.element(element)) {
let method = 'toggle';
if (typeof force !== 'undefined') {
method = force ? 'add' : 'remove';
}
element.classList[method](className);
return element.classList.contains(className);
}
return null;
},
// Has class name
hasClass(element, className) {
return utils.is.element(element) && element.classList.contains(className);
},
// Element matches selector
matches(element, selector) {
const prototype = { Element };
function match() {
return Array.from(document.querySelectorAll(selector)).includes(this);
}
const matches = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;
return matches.call(element, selector);
},
// Find all elements
getElements(selector) {
return this.elements.container.querySelectorAll(selector);
},
// Find a single element
getElement(selector) {
return this.elements.container.querySelector(selector);
},
// Get the focused element
getFocusElement() {
let focused = document.activeElement;
if (!focused || focused === document.body) {
focused = null;
} else {
focused = document.querySelector(':focus');
}
return focused;
},
// Trap focus inside container
trapFocus(element = null, toggle = false) {
if (!utils.is.element(element)) {
return;
}
const focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
const first = focusable[0];
const last = focusable[focusable.length - 1];
const trap = event => {
// Bail if not tab key or not fullscreen
if (event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element
const focused = utils.getFocusElement();
if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used
first.focus();
event.preventDefault();
} else if (focused === first && event.shiftKey) {
// Move focus to last element that can be tabbed if Shift is used
last.focus();
event.preventDefault();
}
};
if (toggle) {
utils.on(this.elements.container, 'keydown', trap, false);
} else {
utils.off(this.elements.container, 'keydown', trap, false);
}
},
// Toggle event listener
toggleListener(elements, event, callback, toggle = false, passive = true, capture = false) {
// Bail if no elemetns, event, or callback
if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) {
return;
}
// If a nodelist is passed, call itself on each node
if (utils.is.nodeList(elements) || utils.is.array(elements)) {
// Create listener for each node
Array.from(elements).forEach(element => {
if (element instanceof Node) {
utils.toggleListener.call(null, element, event, callback, toggle, passive, capture);
}
});
return;
}
// Allow multiple events
const events = event.split(' ');
// Build options
// Default to just the capture boolean for browsers with no passive listener support
let options = capture;
// If passive events listeners are supported
if (support.passiveListeners) {
options = {
// Whether the listener can be passive (i.e. default never prevented)
passive,
// Whether the listener is a capturing listener or not
capture,
};
}
// If a single node is passed, bind the event listener
events.forEach(type => {
elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
});
},
// Bind event handler
on(element, events = '', callback, passive = true, capture = false) {
utils.toggleListener(element, events, callback, true, passive, capture);
},
// Unbind event handler
off(element, events = '', callback, passive = true, capture = false) {
utils.toggleListener(element, events, callback, false, passive, capture);
},
// Trigger event
dispatchEvent(element, type = '', bubbles = false, detail = {}) {
// Bail if no element
if (!utils.is.element(element) || utils.is.empty(type)) {
return;
}
// Create and dispatch the event
const event = new CustomEvent(type, {
bubbles,
detail: Object.assign({}, detail, {
plyr: this,
}),
});
// Dispatch the event
element.dispatchEvent(event);
},
// Toggle aria-pressed state on a toggle button
// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
toggleState(element, input) {
// If multiple elements passed
if (utils.is.array(element) || utils.is.nodeList(element)) {
Array.from(element).forEach(target => utils.toggleState(target, input));
return;
}
// Bail if no target
if (!utils.is.element(element)) {
return;
}
// Get state
const pressed = element.getAttribute('aria-pressed') === 'true';
const state = utils.is.boolean(input) ? input : !pressed;
// Set the attribute on target
element.setAttribute('aria-pressed', state);
},
// Format string
format(input, ...args) {
if (utils.is.empty(input)) {
return input;
}
return input.toString().replace(/{(\d+)}/g, (match, i) => (utils.is.string(args[i]) ? args[i] : ''));
},
// Get percentage
getPercentage(current, max) {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
return 0;
}
return (current / max * 100).toFixed(2);
},
// Time helpers
getHours(value) {
return parseInt((value / 60 / 60) % 60, 10);
},
getMinutes(value) {
return parseInt((value / 60) % 60, 10);
},
getSeconds(value) {
return parseInt(value % 60, 10);
},
// Format time to UI friendly string
formatTime(time = 0, displayHours = false, inverted = false) {
// Bail if the value isn't a number
if (!utils.is.number(time)) {
return utils.formatTime(null, displayHours, inverted);
}
// Format time component to add leading zero
const format = value => `0${value}`.slice(-2);
// Breakdown to hours, mins, secs
let hours = utils.getHours(time);
const mins = utils.getMinutes(time);
const secs = utils.getSeconds(time);
// Do we need to display hours?
if (displayHours || hours > 0) {
hours = `${hours}:`;
} else {
hours = '';
}
// Render
return `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
},
// Replace all occurances of a string in a string
replaceAll(input = '', find = '', replace = '') {
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
},
// Convert to title case
toTitleCase(input = '') {
return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
},
// Convert string to pascalCase
toPascalCase(input = '') {
let string = input.toString();
// Convert kebab case
string = utils.replaceAll(string, '-', ' ');
// Convert snake case
string = utils.replaceAll(string, '_', ' ');
// Convert to title case
string = utils.toTitleCase(string);
// Convert to pascal case
return utils.replaceAll(string, ' ', '');
},
// Convert string to pascalCase
toCamelCase(input = '') {
let string = input.toString();
// Convert to pascal case
string = utils.toPascalCase(string);
// Convert first character to lowercase
return string.charAt(0).toLowerCase() + string.slice(1);
},
// Deep extend destination object with N more objects
extend(target = {}, ...sources) {
if (!sources.length) {
return target;
}
const source = sources.shift();
if (!utils.is.object(source)) {
return target;
}
Object.keys(source).forEach(key => {
if (utils.is.object(source[key])) {
if (!Object.keys(target).includes(key)) {
Object.assign(target, { [key]: {} });
}
utils.extend(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
});
return utils.extend(target, ...sources);
},
// Remove duplicates in an array
dedupe(array) {
if (!utils.is.array(array)) {
return array;
}
return array.filter((item, index) => array.indexOf(item) === index);
},
// Clone nested objects
cloneDeep(object) {
return JSON.parse(JSON.stringify(object));
},
// Get a nested value in an object
getDeep(object, path) {
return path.split('.').reduce((obj, key) => obj && obj[key], object);
},
// Get the closest value in an array
closest(array, value) {
if (!utils.is.array(array) || !array.length) {
return null;
}
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
},
// Get the provider for a given URL
getProviderByUrl(url) {
// YouTube
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) {
return providers.youtube;
}
// Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo;
}
return null;
},
// Parse YouTube ID from URL
parseYouTubeId(url) {
if (utils.is.empty(url)) {
return null;
}
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
return url.match(regex) ? RegExp.$2 : url;
},
// Parse Vimeo ID from URL
parseVimeoId(url) {
if (utils.is.empty(url)) {
return null;
}
if (utils.is.number(Number(url))) {
return url;
}
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
return url.match(regex) ? RegExp.$2 : url;
},
// Convert a URL to a location object
parseUrl(url) {
const parser = document.createElement('a');
parser.href = url;
return parser;
},
// Get URL query parameters
getUrlParams(input) {
let search = input;
// Parse URL if needed
if (input.startsWith('http://') || input.startsWith('https://')) {
({ search } = utils.parseUrl(input));
}
if (utils.is.empty(search)) {
return null;
}
const hashes = search.slice(search.indexOf('?') + 1).split('&');
return hashes.reduce((params, hash) => {
const [
key,
val,
] = hash.split('=');
return Object.assign(params, { [key]: decodeURIComponent(val) });
}, {});
},
// Convert object to URL parameters
buildUrlParams(input) {
if (!utils.is.object(input)) {
return '';
}
return Object.keys(input)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(input[key])}`)
.join('&');
},
// Remove HTML from a string
stripHTML(source) {
const fragment = document.createDocumentFragment();
const element = document.createElement('div');
fragment.appendChild(element);
element.innerHTML = source;
return fragment.firstChild.innerText;
},
// Like outerHTML, but also works for DocumentFragment
getHTML(element) {
const wrapper = document.createElement('div');
wrapper.appendChild(element);
return wrapper.innerHTML;
},
// Get aspect ratio for dimensions
getAspectRatio(width, height) {
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
const ratio = getRatio(width, height);
return `${width / ratio}:${height / ratio}`;
},
// Get the transition end event
get transitionEndEvent() {
const element = document.createElement('span');
const events = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend',
};
const type = Object.keys(events).find(event => element.style[event] !== undefined);
return utils.is.string(type) ? events[type] : false;
},
// Force repaint of element
repaint(element) {
setTimeout(() => {
utils.toggleHidden(element, true);
element.offsetHeight; // eslint-disable-line
utils.toggleHidden(element, false);
}, 0);
},
};
export default utils;
+34
View File
@@ -0,0 +1,34 @@
// ==========================================================================
// Animation utils
// ==========================================================================
import { toggleHidden } from './elements';
import is from './is';
export const transitionEndEvent = (() => {
const element = document.createElement('span');
const events = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend',
};
const type = Object.keys(events).find(event => element.style[event] !== undefined);
return is.string(type) ? events[type] : false;
})();
// Force repaint of element
export function repaint(element) {
setTimeout(() => {
try {
toggleHidden(element, true);
element.offsetHeight; // eslint-disable-line
toggleHidden(element, false);
} catch (e) {
// Do nothing
}
}, 0);
}
+23
View File
@@ -0,0 +1,23 @@
// ==========================================================================
// Array utils
// ==========================================================================
import is from './is';
// Remove duplicates in an array
export function dedupe(array) {
if (!is.array(array)) {
return array;
}
return array.filter((item, index) => array.indexOf(item) === index);
}
// Get the closest value in an array
export function closest(array, value) {
if (!is.array(array) || !array.length) {
return null;
}
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
}
+13
View File
@@ -0,0 +1,13 @@
// ==========================================================================
// Browser sniffing
// Unfortunately, due to mixed support, UA sniffing is required
// ==========================================================================
const browser = {
isIE: /* @cc_on!@ */ false || !!document.documentMode,
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform),
};
export default browser;
+302
View File
@@ -0,0 +1,302 @@
// ==========================================================================
// Element utils
// ==========================================================================
import { toggleListener } from './events';
import is from './is';
// Wrap an element
export function wrap(elements, wrapper) {
// Convert `elements` to an array, if necessary.
const targets = elements.length ? elements : [elements];
// Loops backwards to prevent having to clone the wrapper on the
// first element (see `child` below).
Array.from(targets)
.reverse()
.forEach((element, index) => {
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
// Cache the current parent and sibling.
const parent = element.parentNode;
const sibling = element.nextSibling;
// Wrap the element (is automatically removed from its current
// parent).
child.appendChild(element);
// If the element had a sibling, insert the wrapper before
// the sibling to maintain the HTML structure; otherwise, just
// append it to the parent.
if (sibling) {
parent.insertBefore(child, sibling);
} else {
parent.appendChild(child);
}
});
}
// Set attributes
export function setAttributes(element, attributes) {
if (!is.element(element) || is.empty(attributes)) {
return;
}
// Assume null and undefined attributes should be left out,
// Setting them would otherwise convert them to "null" and "undefined"
Object.entries(attributes)
.filter(([, value]) => !is.nullOrUndefined(value))
.forEach(([key, value]) => element.setAttribute(key, value));
}
// Create a DocumentFragment
export function createElement(type, attributes, text) {
// Create a new <element>
const element = document.createElement(type);
// Set all passed attributes
if (is.object(attributes)) {
setAttributes(element, attributes);
}
// Add text node
if (is.string(text)) {
element.innerText = text;
}
// Return built element
return element;
}
// Inaert an element after another
export function insertAfter(element, target) {
if (!is.element(element) || !is.element(target)) {
return;
}
target.parentNode.insertBefore(element, target.nextSibling);
}
// Insert a DocumentFragment
export function insertElement(type, parent, attributes, text) {
if (!is.element(parent)) {
return;
}
parent.appendChild(createElement(type, attributes, text));
}
// Remove element(s)
export function removeElement(element) {
if (is.nodeList(element) || is.array(element)) {
Array.from(element).forEach(removeElement);
return;
}
if (!is.element(element) || !is.element(element.parentNode)) {
return;
}
element.parentNode.removeChild(element);
}
// Remove all child elements
export function emptyElement(element) {
if (!is.element(element)) {
return;
}
let { length } = element.childNodes;
while (length > 0) {
element.removeChild(element.lastChild);
length -= 1;
}
}
// Replace element
export function replaceElement(newChild, oldChild) {
if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) {
return null;
}
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
}
// Get an attribute object from a string selector
export function getAttributesFromSelector(sel, existingAttributes) {
// For example:
// '.test' to { class: 'test' }
// '#test' to { id: 'test' }
// '[data-test="test"]' to { 'data-test': 'test' }
if (!is.string(sel) || is.empty(sel)) {
return {};
}
const attributes = {};
const existing = existingAttributes;
sel.split(',').forEach(s => {
// Remove whitespace
const selector = s.trim();
const className = selector.replace('.', '');
const stripped = selector.replace(/[[\]]/g, '');
// Get the parts and value
const parts = stripped.split('=');
const key = parts[0];
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
// Get the first character
const start = selector.charAt(0);
switch (start) {
case '.':
// Add to existing classname
if (is.object(existing) && is.string(existing.class)) {
existing.class += ` ${className}`;
}
attributes.class = className;
break;
case '#':
// ID selector
attributes.id = selector.replace('#', '');
break;
case '[':
// Attribute selector
attributes[key] = value;
break;
default:
break;
}
});
return attributes;
}
// Toggle hidden
export function toggleHidden(element, hidden) {
if (!is.element(element)) {
return;
}
let hide = hidden;
if (!is.boolean(hide)) {
hide = !element.hidden;
}
if (hide) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
}
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
export function toggleClass(element, className, force) {
if (is.nodeList(element)) {
return Array.from(element).map(e => toggleClass(e, className, force));
}
if (is.element(element)) {
let method = 'toggle';
if (typeof force !== 'undefined') {
method = force ? 'add' : 'remove';
}
element.classList[method](className);
return element.classList.contains(className);
}
return false;
}
// Has class name
export function hasClass(element, className) {
return is.element(element) && element.classList.contains(className);
}
// Element matches selector
export function matches(element, selector) {
const prototype = { Element };
function match() {
return Array.from(document.querySelectorAll(selector)).includes(this);
}
const matches =
prototype.matches ||
prototype.webkitMatchesSelector ||
prototype.mozMatchesSelector ||
prototype.msMatchesSelector ||
match;
return matches.call(element, selector);
}
// Find all elements
export function getElements(selector) {
return this.elements.container.querySelectorAll(selector);
}
// Find a single element
export function getElement(selector) {
return this.elements.container.querySelector(selector);
}
// 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)) {
return;
}
// Set regular focus
element.focus({ preventScroll: true });
// If we want to mimic keyboard focus via tab
if (tabFocus) {
toggleClass(element, this.config.classNames.tabFocus);
}
}
+120
View File
@@ -0,0 +1,120 @@
// ==========================================================================
// Event utils
// ==========================================================================
import is from './is';
// Check for passive event listener support
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
// https://www.youtube.com/watch?v=NPM6172J22g
const supportsPassiveListeners = (() => {
// Test via a getter in the options object to see if the passive property is accessed
let supported = false;
try {
const options = Object.defineProperty({}, 'passive', {
get() {
supported = true;
return null;
},
});
window.addEventListener('test', null, options);
window.removeEventListener('test', null, options);
} catch (e) {
// Do nothing
}
return supported;
})();
// Toggle event listener
export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {
// Bail if no element, event, or callback
if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {
return;
}
// Allow multiple events
const events = event.split(' ');
// Build options
// Default to just the capture boolean for browsers with no passive listener support
let options = capture;
// If passive events listeners are supported
if (supportsPassiveListeners) {
options = {
// Whether the listener can be passive (i.e. default never prevented)
passive,
// Whether the listener is a capturing listener or not
capture,
};
}
// If a single node is passed, bind the event listener
events.forEach(type => {
if (this && this.eventListeners && toggle) {
// Cache event listener
this.eventListeners.push({ element, type, callback, options });
}
element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
});
}
// Bind event handler
export function on(element, events = '', callback, passive = true, capture = false) {
toggleListener.call(this, element, events, callback, true, passive, capture);
}
// Unbind event handler
export function off(element, events = '', callback, passive = true, capture = false) {
toggleListener.call(this, element, events, callback, false, passive, capture);
}
// Bind once-only event handler
export function once(element, events = '', callback, passive = true, capture = false) {
function onceCallback(...args) {
off(element, events, onceCallback, passive, capture);
callback.apply(this, args);
}
toggleListener.call(this, element, events, onceCallback, true, passive, capture);
}
// Trigger event
export function triggerEvent(element, type = '', bubbles = false, detail = {}) {
// Bail if no element
if (!is.element(element) || is.empty(type)) {
return;
}
// Create and dispatch the event
const event = new CustomEvent(type, {
bubbles,
detail: Object.assign({}, detail, {
plyr: this,
}),
});
// Dispatch the event
element.dispatchEvent(event);
}
// Unbind all cached event listeners
export function unbindListeners() {
if (this && this.eventListeners) {
this.eventListeners.forEach(item => {
const { element, type, callback, options } = item;
element.removeEventListener(type, callback, options);
});
this.eventListeners = [];
}
}
// Run method when / if player is ready
export function ready() {
return new Promise(
resolve => (this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)),
).then(() => {});
}
+42
View File
@@ -0,0 +1,42 @@
// ==========================================================================
// Fetch wrapper
// Using XHR to avoid issues with older browsers
// ==========================================================================
export default function fetch(url, responseType = 'text') {
return new Promise((resolve, reject) => {
try {
const request = new XMLHttpRequest();
// Check for CORS support
if (!('withCredentials' in request)) {
return;
}
request.addEventListener('load', () => {
if (responseType === 'text') {
try {
resolve(JSON.parse(request.responseText));
} catch (e) {
resolve(request.responseText);
}
} else {
resolve(request.response);
}
});
request.addEventListener('error', () => {
throw new Error(request.status);
});
request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send();
} catch (e) {
reject(e);
}
});
}
+47
View File
@@ -0,0 +1,47 @@
// ==========================================================================
// Plyr internationalization
// ==========================================================================
import is from './is';
import { getDeep } from './objects';
import { replaceAll } from './strings';
// Skip i18n for abbreviations and brand names
const resources = {
pip: 'PIP',
airplay: 'AirPlay',
html5: 'HTML5',
vimeo: 'Vimeo',
youtube: 'YouTube',
};
const i18n = {
get(key = '', config = {}) {
if (is.empty(key) || is.empty(config)) {
return '';
}
let string = getDeep(config.i18n, key);
if (is.empty(string)) {
if (Object.keys(resources).includes(key)) {
return resources[key];
}
return '';
}
const replace = {
'{seektime}': config.seekTime,
'{title}': config.title,
};
Object.entries(replace).forEach(([key, value]) => {
string = replaceAll(string, key, value);
});
return string;
},
};
export default i18n;
+70
View File
@@ -0,0 +1,70 @@
// ==========================================================================
// Type checking utils
// ==========================================================================
const getConstructor = input => (input !== null && typeof input !== 'undefined' ? input.constructor : null);
const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);
const isNullOrUndefined = input => input === null || typeof input === 'undefined';
const isObject = input => getConstructor(input) === Object;
const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);
const isString = input => getConstructor(input) === String;
const isBoolean = input => getConstructor(input) === Boolean;
const isFunction = input => getConstructor(input) === Function;
const isArray = input => Array.isArray(input);
const isWeakMap = input => instanceOf(input, WeakMap);
const isNodeList = input => instanceOf(input, NodeList);
const isElement = input => instanceOf(input, Element);
const isTextNode = input => getConstructor(input) === Text;
const isEvent = input => instanceOf(input, Event);
const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);
const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));
const isEmpty = input =>
isNullOrUndefined(input) ||
((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||
(isObject(input) && !Object.keys(input).length);
const isUrl = input => {
// Accept a URL object
if (instanceOf(input, window.URL)) {
return true;
}
// Must be string from here
if (!isString(input)) {
return false;
}
// Add the protocol if required
let string = input;
if (!input.startsWith('http://') || !input.startsWith('https://')) {
string = `http://${input}`;
}
try {
return !isEmpty(new URL(string).hostname);
} catch (e) {
return false;
}
};
export default {
nullOrUndefined: isNullOrUndefined,
object: isObject,
number: isNumber,
string: isString,
boolean: isBoolean,
function: isFunction,
array: isArray,
weakMap: isWeakMap,
nodeList: isNodeList,
element: isElement,
textNode: isTextNode,
event: isEvent,
keyboardEvent: isKeyboardEvent,
cue: isCue,
track: isTrack,
url: isUrl,
empty: isEmpty,
};
+19
View File
@@ -0,0 +1,19 @@
// ==========================================================================
// Load image avoiding xhr/fetch CORS issues
// Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded
// By default it checks if it is at least 1px, but you can add a second argument to change this
// ==========================================================================
export default function loadImage(src, minWidth = 1) {
return new Promise((resolve, reject) => {
const image = new Image();
const handler = () => {
delete image.onload;
delete image.onerror;
(image.naturalWidth >= minWidth ? resolve : reject)(image);
};
Object.assign(image, { onload: handler, onerror: handler, src });
});
}
+14
View File
@@ -0,0 +1,14 @@
// ==========================================================================
// Load an external script
// ==========================================================================
import loadjs from 'loadjs';
export default function loadScript(url) {
return new Promise((resolve, reject) => {
loadjs(url, {
success: resolve,
error: reject,
});
});
}
+76
View File
@@ -0,0 +1,76 @@
// ==========================================================================
// Sprite loader
// ==========================================================================
import Storage from '../storage';
import fetch from './fetch';
import is from './is';
// Load an external SVG sprite
export default function loadSprite(url, id) {
if (!is.string(url)) {
return;
}
const prefix = 'cache';
const hasId = is.string(id);
let isCached = false;
const exists = () => document.getElementById(id) !== null;
const update = (container, data) => {
container.innerHTML = data;
// Check again incase of race condition
if (hasId && exists()) {
return;
}
// Inject the SVG to the body
document.body.insertAdjacentElement('afterbegin', container);
};
// Only load once if ID set
if (!hasId || !exists()) {
const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
container.setAttribute('hidden', '');
if (hasId) {
container.setAttribute('id', id);
}
// Check in cache
if (useStorage) {
const cached = window.localStorage.getItem(`${prefix}-${id}`);
isCached = cached !== null;
if (isCached) {
const data = JSON.parse(cached);
update(container, data.content);
}
}
// Get the sprite
fetch(url)
.then(result => {
if (is.empty(result)) {
return;
}
if (useStorage) {
window.localStorage.setItem(
`${prefix}-${id}`,
JSON.stringify({
content: result,
}),
);
}
update(container, result);
})
.catch(() => {});
}
}
+42
View File
@@ -0,0 +1,42 @@
// ==========================================================================
// Object utils
// ==========================================================================
import is from './is';
// Clone nested objects
export function cloneDeep(object) {
return JSON.parse(JSON.stringify(object));
}
// Get a nested value in an object
export function getDeep(object, path) {
return path.split('.').reduce((obj, key) => obj && obj[key], object);
}
// Deep extend destination object with N more objects
export function extend(target = {}, ...sources) {
if (!sources.length) {
return target;
}
const source = sources.shift();
if (!is.object(source)) {
return target;
}
Object.keys(source).forEach(key => {
if (is.object(source[key])) {
if (!Object.keys(target).includes(key)) {
Object.assign(target, { [key]: {} });
}
extend(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
});
return extend(target, ...sources);
}
+85
View File
@@ -0,0 +1,85 @@
// ==========================================================================
// String utils
// ==========================================================================
import is from './is';
// Generate a random ID
export function generateId(prefix) {
return `${prefix}-${Math.floor(Math.random() * 10000)}`;
}
// Format string
export function format(input, ...args) {
if (is.empty(input)) {
return input;
}
return input.toString().replace(/{(\d+)}/g, (match, i) => args[i].toString());
}
// Get percentage
export function getPercentage(current, max) {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
return 0;
}
return ((current / max) * 100).toFixed(2);
}
// Replace all occurances of a string in a string
export function replaceAll(input = '', find = '', replace = '') {
return input.replace(
new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'),
replace.toString(),
);
}
// Convert to title case
export function toTitleCase(input = '') {
return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
}
// Convert string to pascalCase
export function toPascalCase(input = '') {
let string = input.toString();
// Convert kebab case
string = replaceAll(string, '-', ' ');
// Convert snake case
string = replaceAll(string, '_', ' ');
// Convert to title case
string = toTitleCase(string);
// Convert to pascal case
return replaceAll(string, ' ', '');
}
// Convert string to pascalCase
export function toCamelCase(input = '') {
let string = input.toString();
// Convert to pascal case
string = toPascalCase(string);
// Convert first character to lowercase
return string.charAt(0).toLowerCase() + string.slice(1);
}
// Remove HTML from a string
export function stripHTML(source) {
const fragment = document.createDocumentFragment();
const element = document.createElement('div');
fragment.appendChild(element);
element.innerHTML = source;
return fragment.firstChild.innerText;
}
// Like outerHTML, but also works for DocumentFragment
export function getHTML(element) {
const wrapper = document.createElement('div');
wrapper.appendChild(element);
return wrapper.innerHTML;
}
+36
View File
@@ -0,0 +1,36 @@
// ==========================================================================
// Time utils
// ==========================================================================
import is from './is';
// Time helpers
export const getHours = value => parseInt((value / 60 / 60) % 60, 10);
export const getMinutes = value => parseInt((value / 60) % 60, 10);
export const getSeconds = value => parseInt(value % 60, 10);
// Format time to UI friendly string
export function formatTime(time = 0, displayHours = false, inverted = false) {
// Bail if the value isn't a number
if (!is.number(time)) {
return formatTime(null, displayHours, inverted);
}
// Format time component to add leading zero
const format = value => `0${value}`.slice(-2);
// Breakdown to hours, mins, secs
let hours = getHours(time);
const mins = getMinutes(time);
const secs = getSeconds(time);
// Do we need to display hours?
if (displayHours || hours > 0) {
hours = `${hours}:`;
} else {
hours = '';
}
// Render
return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
}
+39
View File
@@ -0,0 +1,39 @@
// ==========================================================================
// URL utils
// ==========================================================================
import is from './is';
/**
* Parse a string to a URL object
* @param {string} input - the URL to be parsed
* @param {boolean} safe - failsafe parsing
*/
export function parseUrl(input, safe = true) {
let url = input;
if (safe) {
const parser = document.createElement('a');
parser.href = url;
url = parser.href;
}
try {
return new URL(url);
} catch (e) {
return null;
}
}
// Convert object to URLSearchParams
export function buildUrlParams(input) {
const params = new URLSearchParams();
if (is.object(input)) {
Object.entries(input).forEach(([key, value]) => {
params.set(key, value);
});
}
return params;
}
+4 -3
View File
@@ -17,7 +17,6 @@
padding: $plyr-control-spacing;
position: absolute;
text-align: center;
transform: translateY(-($plyr-control-spacing * 4));
transition: transform 0.4s ease-in-out;
width: 100%;
@@ -53,6 +52,8 @@
display: block;
}
.plyr--hide-controls .plyr__captions {
transform: translateY(-($plyr-control-spacing * 1.5));
// If the lower controls are shown and not empty
.plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty) ~ .plyr__captions {
transform: translateY(-($plyr-control-spacing * 4));
}
+32 -8
View File
@@ -33,15 +33,25 @@
}
}
// Remove any link styling
a.plyr__control {
text-decoration: none;
&::after,
&::before {
display: none;
}
}
// Change icons on state change
.plyr__control[aria-pressed='false'] .icon--pressed,
.plyr__control[aria-pressed='true'] .icon--not-pressed,
.plyr__control[aria-pressed='false'] .label--pressed,
.plyr__control[aria-pressed='true'] .label--not-pressed {
.plyr__control:not(.plyr__control--pressed) .icon--pressed,
.plyr__control.plyr__control--pressed .icon--not-pressed,
.plyr__control:not(.plyr__control--pressed) .label--pressed,
.plyr__control.plyr__control--pressed .label--not-pressed {
display: none;
}
// Audio styles
// Audio control
.plyr--audio .plyr__control {
&.plyr__tab-focus,
&:hover,
@@ -51,6 +61,21 @@
}
}
// 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,
&[aria-expanded='true'] {
background: $plyr-video-control-bg-hover;
color: $plyr-video-control-color-hover;
}
}
// Large play button (video only)
.plyr__control--overlaid {
background: rgba($plyr-video-control-bg-hover, 0.8);
@@ -66,11 +91,10 @@
transform: translate(-50%, -50%);
z-index: 2;
// Offset icon to make the play button look right
svg {
height: $plyr-control-icon-size-large;
left: 2px; // Offset to make the play button look right
left: 2px;
position: relative;
width: $plyr-control-icon-size-large;
}
&:hover,
+45 -46
View File
@@ -11,67 +11,44 @@
.plyr__controls {
align-items: center;
display: flex;
justify-content: flex-end;
text-align: center;
// Spacing
> .plyr__control,
.plyr__progress,
.plyr__time,
.plyr__menu {
margin-left: ($plyr-control-spacing / 2);
&:first-child,
&:first-child + [data-plyr='pause'] {
margin-left: 0;
}
}
.plyr__menu,
.plyr__volume {
margin-left: ($plyr-control-spacing / 2);
}
.plyr__menu + .plyr__control,
> .plyr__control + .plyr__menu,
> .plyr__control + .plyr__control,
.plyr__progress + .plyr__control {
margin-left: floor($plyr-control-spacing / 4);
}
> .plyr__control:first-child,
> .plyr__control:first-child + [data-plyr='pause'] {
margin-left: 0;
margin-right: auto;
}
// Hide empty controls
&:empty {
display: none;
}
@media (min-width: $plyr-bp-sm) {
> .plyr__control,
.plyr__menu,
.plyr__progress,
.plyr__time,
.plyr__menu {
.plyr__volume {
margin-left: $plyr-control-spacing;
}
> .plyr__control + .plyr__control,
.plyr__menu + .plyr__control,
> .plyr__control + .plyr__menu {
margin-left: ($plyr-control-spacing / 2);
}
}
}
// Video controls
.plyr--video .plyr__controls {
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;
color: $plyr-video-control-color;
left: 0;
padding: ($plyr-control-spacing * 3.5) $plyr-control-spacing $plyr-control-spacing;
position: absolute;
right: 0;
transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
z-index: 2;
.plyr__control {
svg {
filter: drop-shadow(0 1px 1px rgba(#000, 0.15));
}
// Hover and tab focus
&.plyr__tab-focus,
&:hover,
&[aria-expanded='true'] {
background: $plyr-video-control-bg-hover;
color: $plyr-video-control-color-hover;
}
}
}
@@ -83,7 +60,29 @@
padding: $plyr-control-spacing;
}
// Hide controls
// Video controls
.plyr--video .plyr__controls {
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;
color: $plyr-video-control-color;
left: 0;
padding: ($plyr-control-spacing * 2) ($plyr-control-spacing / 2) ($plyr-control-spacing / 2);
position: absolute;
right: 0;
transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
z-index: 3;
@media (min-width: $plyr-bp-sm) {
padding: ($plyr-control-spacing * 3.5) $plyr-control-spacing $plyr-control-spacing;
}
}
// Hide video controls
.plyr--video.plyr--hide-controls .plyr__controls {
opacity: 0;
pointer-events: none;
-5
View File
@@ -27,11 +27,6 @@ $embed-padding: ((100 / 16) * 9);
$height: 240;
$offset: to-percentage(($height - $embed-padding) / ($height / 50));
// To allow mouse events to be captured if full support
iframe {
pointer-events: none;
}
// Only used for Vimeo
> .plyr__video-embed__container {
padding-bottom: to-percentage($height);
+44 -39
View File
@@ -39,7 +39,8 @@
> div {
overflow: hidden;
transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1), width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1),
width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
// Arrow
@@ -54,18 +55,16 @@
width: 0;
}
ul {
list-style: none;
margin: 0;
overflow: hidden;
[role='menu'] {
padding: $plyr-control-padding;
}
li {
margin-top: 2px;
[role='menuitem'],
[role='menuitemradio'] {
margin-top: 2px;
&:first-child {
margin-top: 0;
}
&:first-child {
margin-top: 0;
}
}
@@ -75,10 +74,17 @@
color: $plyr-menu-color;
display: flex;
font-size: $plyr-font-size-menu;
padding: ceil($plyr-control-padding / 2) ($plyr-control-padding * 2);
padding: ceil($plyr-control-padding / 2)
ceil($plyr-control-padding * 1.5);
user-select: none;
width: 100%;
> span {
align-items: inherit;
display: flex;
width: 100%;
}
&::after {
border: 4px solid transparent;
content: '';
@@ -135,50 +141,49 @@
}
}
label.plyr__control {
.plyr__control[role='menuitemradio'] {
padding-left: $plyr-control-padding;
input[type='radio'] + span {
background: rgba(#000, 0.1);
&::before,
&::after {
border-radius: 100%;
}
&::before {
background: rgba(#000, 0.1);
content: '';
display: block;
flex-shrink: 0;
height: 16px;
margin-right: $plyr-control-spacing;
position: relative;
transition: all 0.3s ease;
width: 16px;
&::after {
background: #fff;
border-radius: 100%;
content: '';
height: 6px;
left: 5px;
opacity: 0;
position: absolute;
top: 5px;
transform: scale(0);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 6px;
}
}
input[type='radio']:checked + span {
background: $plyr-color-main;
&::after {
background: #fff;
border: 0;
height: 6px;
left: 12px;
opacity: 0;
top: 50%;
transform: translateY(-50%) scale(0);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 6px;
}
&[aria-checked='true'] {
&::before {
background: $plyr-color-main;
}
&::after {
opacity: 1;
transform: scale(1);
transform: translateY(-50%) scale(1);
}
}
input[type='radio']:focus + span {
@include plyr-tab-focus();
}
&.plyr__tab-focus input[type='radio'] + span,
&:hover input[type='radio'] + span {
&.plyr__tab-focus::before,
&:hover::before {
background: rgba(#000, 0.1);
}
}
@@ -188,7 +193,7 @@
align-items: center;
display: flex;
margin-left: auto;
margin-right: -$plyr-control-padding;
margin-right: -($plyr-control-padding - 2);
overflow: hidden;
padding-left: ceil($plyr-control-padding * 3.5);
pointer-events: none;
+1 -2
View File
@@ -12,10 +12,9 @@
opacity: 0;
position: absolute;
top: 0;
transition: opacity 0.3s ease;
transition: opacity 0.2s ease;
width: 100%;
z-index: 1;
pointer-events: none;
}
.plyr--stopped.plyr__poster-enabled .plyr__poster {
-1
View File
@@ -3,7 +3,6 @@
// --------------------------------------------------------------
.plyr__progress {
display: flex;
flex: 1;
left: $plyr-range-thumb-height / 2;
margin-right: $plyr-range-thumb-height;
+14 -4
View File
@@ -19,7 +19,11 @@
&::-webkit-slider-runnable-track {
@include plyr-range-track();
background-image: linear-gradient(to right, currentColor var(--value, 0%), transparent var(--value, 0%));
background-image: linear-gradient(
to right,
currentColor var(--value, 0%),
transparent var(--value, 0%)
);
}
&::-webkit-slider-thumb {
@@ -140,15 +144,21 @@
// Pressed styles
&:active {
&::-webkit-slider-thumb {
@include plyr-range-thumb-active($plyr-audio-range-thumb-shadow-color);
@include plyr-range-thumb-active(
$plyr-audio-range-thumb-shadow-color
);
}
&::-moz-range-thumb {
@include plyr-range-thumb-active($plyr-audio-range-thumb-shadow-color);
@include plyr-range-thumb-active(
$plyr-audio-range-thumb-shadow-color
);
}
&::-ms-thumb {
@include plyr-range-thumb-active($plyr-audio-range-thumb-shadow-color);
@include plyr-range-thumb-active(
$plyr-audio-range-thumb-shadow-color
);
}
}
}
+2
View File
@@ -10,6 +10,7 @@
color: $plyr-tooltip-color;
font-size: $plyr-font-size-small;
font-weight: $plyr-font-weight-regular;
left: 50%;
line-height: 1.3;
margin-bottom: ($plyr-tooltip-padding * 2);
opacity: 0;
@@ -64,6 +65,7 @@
// Last tooltip
.plyr__controls > .plyr__control:last-child .plyr__tooltip {
left: auto;
right: 0;
transform: translate(0, 10px) scale(0.8);
transform-origin: 100% 100%;
+1
View File
@@ -3,6 +3,7 @@
// --------------------------------------------------------------
.plyr--video {
background: #000;
overflow: hidden;
// Menu open
+5 -2
View File
@@ -3,20 +3,23 @@
// --------------------------------------------------------------
.plyr__volume {
align-items: center;
display: flex;
flex: 1;
position: relative;
input[type='range'] {
margin-left: ($plyr-control-spacing / 2);
position: relative;
z-index: 2;
}
@media (min-width: $plyr-bp-sm) {
max-width: 50px;
max-width: 90px;
}
@media (min-width: $plyr-bp-md) {
max-width: 80px;
max-width: 110px;
}
}
+2 -3
View File
@@ -5,7 +5,7 @@
// Nicer focus styles
// ---------------------------------------
@mixin plyr-tab-focus($color: $plyr-tab-focus-default-color) {
box-shadow: 0 0 0 3px rgba($color, 0.35);
box-shadow: 0 0 0 5px rgba($color, 0.5);
outline: 0;
}
@@ -28,7 +28,7 @@
border: 0;
border-radius: ($plyr-range-track-height / 2);
height: $plyr-range-track-height;
transition: all 0.3s ease;
transition: box-shadow 0.3s ease;
user-select: none;
}
@@ -37,7 +37,6 @@
border: 0;
border-radius: 100%;
box-shadow: $plyr-range-thumb-shadow;
box-sizing: border-box;
height: $plyr-range-thumb-height;
position: relative;
transition: all 0.2s ease;
-1
View File
@@ -3,7 +3,6 @@
// ==========================================================================
$plyr-control-icon-size: 18px !default;
$plyr-control-icon-size-large: 20px !default;
$plyr-control-spacing: 10px !default;
$plyr-control-padding: ($plyr-control-spacing * 0.7) !default;
$plyr-control-radius: 3px !default;
+1 -1
View File
@@ -12,7 +12,7 @@ $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;
// Track
$plyr-range-track-height: 6px !default;
$plyr-range-track-height: 4px !default;
$plyr-range-max-height: ($plyr-range-thumb-active-shadow-width * 2) + $plyr-range-thumb-height !default;
// Fill
+1 -1
View File
@@ -17,4 +17,4 @@ $plyr-font-weight-bold: 600 !default;
$plyr-line-height: 1.7 !default;
$plyr-font-smoothing: true !default;
$plyr-font-smoothing: false !default;
+4
View File
@@ -22,3 +22,7 @@
width: 1px;
}
}
.plyr [hidden] {
display: none !important;
}
+1 -2
View File
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="M16,1 L2,1 C1.447,1 1,1.447 1,2 L1,12 C1,12.553 1.447,13 2,13 L5,13 L5,11 L3,11 L3,3 L15,3 L15,11 L13,11 L13,13 L16,13 C16.553,13 17,12.553 17,12 L17,2 C17,1.447 16.553,1 16,1 L16,1 Z"></path>
<polygon points="4 17 14 17 9 11"></polygon>

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 374 B

+1 -3
View File
@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<g fill-rule="evenodd" fill-opacity="0.5">
<path d="M1,1 C0.4,1 0,1.4 0,2 L0,13 C0,13.6 0.4,14 1,14 L5.6,14 L8.3,16.7 C8.5,16.9 8.7,17 9,17 C9.3,17 9.5,16.9 9.7,16.7 L12.4,14 L17,14 C17.6,14 18,13.6 18,13 L18,2 C18,1.4 17.6,1 17,1 L1,1 Z M5.52,11.15 C7.51,11.15 8.53,9.83 8.8,8.74 L7.51,8.35 C7.32,9.01 6.73,9.8 5.52,9.8 C4.38,9.8 3.32,8.97 3.32,7.46 C3.32,5.85 4.44,5.09 5.5,5.09 C6.73,5.09 7.28,5.84 7.45,6.52 L8.75,6.11 C8.47,4.96 7.46,3.76 5.5,3.76 C3.6,3.76 1.89,5.2 1.89,7.46 C1.89,9.72 3.54,11.15 5.52,11.15 Z M13.09,11.15 C15.08,11.15 16.1,9.83 16.37,8.74 L15.08,8.35 C14.89,9.01 14.3,9.8 13.09,9.8 C11.95,9.8 10.89,8.97 10.89,7.46 C10.89,5.85 12.01,5.09 13.07,5.09 C14.3,5.09 14.85,5.84 15.02,6.52 L16.32,6.11 C16.04,4.96 15.03,3.76 13.07,3.76 C11.17,3.76 9.46,5.2 9.46,7.46 C9.46,9.72 11.11,11.15 13.09,11.15 Z"></path>
</g>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 945 B

+1 -3
View File
@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<g fill-rule="evenodd">
<path d="M1,1 C0.4,1 0,1.4 0,2 L0,13 C0,13.6 0.4,14 1,14 L5.6,14 L8.3,16.7 C8.5,16.9 8.7,17 9,17 C9.3,17 9.5,16.9 9.7,16.7 L12.4,14 L17,14 C17.6,14 18,13.6 18,13 L18,2 C18,1.4 17.6,1 17,1 L1,1 Z M5.52,11.15 C7.51,11.15 8.53,9.83 8.8,8.74 L7.51,8.35 C7.32,9.01 6.73,9.8 5.52,9.8 C4.38,9.8 3.32,8.97 3.32,7.46 C3.32,5.85 4.44,5.09 5.5,5.09 C6.73,5.09 7.28,5.84 7.45,6.52 L8.75,6.11 C8.47,4.96 7.46,3.76 5.5,3.76 C3.6,3.76 1.89,5.2 1.89,7.46 C1.89,9.72 3.54,11.15 5.52,11.15 Z M13.09,11.15 C15.08,11.15 16.1,9.83 16.37,8.74 L15.08,8.35 C14.89,9.01 14.3,9.8 13.09,9.8 C11.95,9.8 10.89,8.97 10.89,7.46 C10.89,5.85 12.01,5.09 13.07,5.09 C14.3,5.09 14.85,5.84 15.02,6.52 L16.32,6.11 C16.04,4.96 15.03,3.76 13.07,3.76 C11.17,3.76 9.46,5.2 9.46,7.46 C9.46,9.72 11.11,11.15 13.09,11.15 Z"></path>
</g>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 926 B

+6
View File
@@ -0,0 +1,6 @@
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(2 1)">
<path d="M7,12 C7.3,12 7.5,11.9 7.7,11.7 L13.4,6 L12,4.6 L8,8.6 L8,0 L6,0 L6,8.6 L2,4.6 L0.6,6 L6.3,11.7 C6.5,11.9 6.7,12 7,12 Z" />
<rect width="14" height="2" y="14" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 325 B

+3 -6
View File
@@ -1,7 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g>
<polygon points="10 3 13.6 3 9.6 7 11 8.4 15 4.4 15 8 17 8 17 1 10 1"></polygon>
<polygon points="7 9.6 3 13.6 3 10 1 10 1 17 8 17 8 15 4.4 15 8.4 11"></polygon>
</g>
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<polygon points="10 3 13.6 3 9.6 7 11 8.4 15 4.4 15 8 17 8 17 1 10 1"></polygon>
<polygon points="7 9.6 3 13.6 3 10 1 10 1 17 8 17 8 15 4.4 15 8.4 11"></polygon>
</svg>

Before

Width:  |  Height:  |  Size: 401 B

After

Width:  |  Height:  |  Size: 264 B

Some files were not shown because too many files have changed in this diff Show More