Compare commits

..

316 Commits

Author SHA1 Message Date
Sam Potts 86c3cf648f v3.6.2 2020-05-04 21:38:10 +10:00
Sam Potts 37be1336fb v3.6.2 2020-05-04 21:33:16 +10:00
Sam Potts 3d7f80737b Package upgrades 2020-05-04 21:32:30 +10:00
Sam Potts 58a9321764 Merge pull request #1818 from Bashev/patch-1
Update tooltips.scss
2020-05-04 21:22:21 +10:00
Sam Potts b600f387f0 Merge pull request #1819 from Bashev/patch-2
Update controls.scss
2020-05-04 21:22:05 +10:00
Kostadin Bashev 2a97adbec6 Update controls.scss 2020-05-01 18:17:49 +03:00
Kostadin Bashev 853753a3da Update tooltips.scss 2020-05-01 18:16:59 +03:00
Sam Potts 405bf9ce37 Merge pull request #1815 from taylorchu/progbar
add missing previewThumbnails options
2020-04-29 10:10:39 +10:00
taylorchu d5ea881729 add missing previewThumbnails options 2020-04-28 17:03:10 -07:00
Sam Potts 391b9acd98 Merge pull request #1811 from taylorchu/better-control-2
allow custom control as element
2020-04-29 09:39:50 +10:00
Sam Potts dda2d072aa Merge pull request #1813 from sampotts/master
Merge back
2020-04-29 09:21:32 +10:00
taylorchu 3c1ba2397b allow custom control as element 2020-04-28 15:28:36 -07:00
Sam Potts adb3f35920 v3.6.1 2020-04-28 23:17:54 +10:00
Sam Potts a58b8bf4bb 2 spaces 2020-04-28 22:22:17 +10:00
Sam Potts 99a26d65cf Merge pull request #1796 from sampotts/develop
v3.6.0
2020-04-28 22:20:49 +10:00
Sam Potts 4915cf0120 Use new feature to set thumbnails on source change in the demo 2020-04-24 01:15:54 +10:00
Sam Potts 66c5780616 v3.6.0 2020-04-24 01:05:58 +10:00
Sam Potts e48b1d11ce Housekeeping 2020-04-24 00:47:41 +10:00
Sam Potts ba91f23c50 Fix linting issues 2020-04-24 00:39:26 +10:00
Sam Potts ad4f303aa0 Merge branch 'master' into develop
# Conflicts:
#	readme.md
2020-04-24 00:34:44 +10:00
Sam Potts 3b521f73bd Merge branch 'develop' of github.com:sampotts/plyr into develop
# Conflicts:
#	package.json
#	yarn.lock
2020-04-24 00:32:16 +10:00
Sam Potts 27126b20bc Merge 2020-04-24 00:31:50 +10:00
Sam Potts 68137aa789 Merge pull request #1770 from sampotts/css-variables
Allow customization via CSS Custom Properties
2020-04-24 00:22:39 +10:00
Sam Potts 2458eaa11b Merge branch 'develop' into css-variables
# Conflicts:
#	src/js/captions.js
#	src/js/config/defaults.js
#	src/js/fullscreen.js
#	src/js/listeners.js
#	src/js/plyr.js
2020-04-24 00:22:17 +10:00
Sam Potts a97008aeeb More work on custom properties and documentation 2020-04-24 00:14:50 +10:00
Sam Potts a9b24f5e1f Use custom properties in demo 2020-04-23 22:07:32 +10:00
Sam Potts a9c4e77d1b Migrate custom properties from media to parent 2020-04-23 22:06:36 +10:00
Sam Potts dbe618c644 Merge pull request #1793 from theprojectsomething/features/postload-captions
Preload TextTracks as per default video element
2020-04-23 22:03:45 +10:00
Som Meaden 40f06e0b4c This commit addresses preloading TextTracks as outlined in feature request #1791
These changes bring Plyr captions download behaviour in line with that of the default video element in major browsers. Specifically text tracks only download as they are required for display. Previously all text tracks would download when the Plyr instance was instantiated - which could become an issue when e.g. many translations are available.

For a track to be downloaded it must either be the default track, the active track when captions are toggled on, or selected from the captions menu.
2020-04-23 17:01:49 +10:00
Sam Potts 6aa21c1fae Merge pull request #1789 from likev/patch-1
Update readme.md
2020-04-22 08:31:23 +10:00
Sam Potts 8015a961cb Merge pull request #1790 from likev/patch-2
Update readme.md
2020-04-22 08:30:23 +10:00
xufanglu d2fa69bca6 Update readme.md
fixed code logic error
2020-04-21 23:25:00 +08:00
xufanglu d63182ecd5 Update readme.md
There is another same Note below
2020-04-21 23:06:06 +08:00
Sam Potts 145f2ae24f Poster image fix (fixes #1763) 2020-04-19 20:06:58 +10:00
Sam Potts 9c7e429b48 Vimeo ratio fixes 2020-04-19 19:51:06 +10:00
Sam Potts 6f1366bd19 Merge pull request #1759 from theprojectsomething/features/fullscreen-container
Features/fullscreen container
2020-04-19 16:51:31 +10:00
Sam Potts 502d5977d7 Converted to 2 space indentation 2020-04-11 16:23:14 +10:00
Som Meaden 353e19e746 revert dist (brain fart earlier .. deleted instead of reverting) 2020-04-05 12:32:17 +10:00
Som Meaden 12ab1ed144 convert fullscreen children listeners logic to ~ES6
remove gulp serve shortcut
2020-04-04 20:00:43 +10:00
Som Meaden 11214caf77 revert demo
feature demo is available here: https://codepen.io/theprojectsomething/full/bGdyJmv
2020-04-04 19:57:38 +10:00
Som Meaden c23b4576df remove dist 2020-04-04 14:21:59 +10:00
Som Meaden a55bf00d0f minor demo fix for fallback mode (inline style was overriding plyr stylesheet) 2020-04-04 14:00:03 +10:00
Som Meaden 49ed2cac4e This is a PR to allow for contextual content to be included in fullscreen (or fallback) mode. This means arbitrary elements (extensions to the basic player UI) can be overlaid and remain visible when the player switches to fullscreen.
Example use-cases include:
 - display of video title or other metadata (see the included demo)
 - alternative access to menu items, such as a searchable captions list (in cases where many hundreds of languages are available)
 - custom share dialogs
 - integrated playlists with 'playing next' overlays

This approach / PR is just an example of how this feature could work and aims to keep Plyr complexity to a minimum (while enabling some fairly interesting integrations). It utilises a single config option, and does away with the need for injecting bespoke APIs or elements into the player context on a per-project basis. Or trying to mess with what is a pretty slick, but tightly coupled system.

For the user: A new `fullscreen.container` attribute is used to provide a container selector. The container must be an ancestor of the player, otherwise it's ignored. When toggling fullscreen mode, this container is now used in place of the player. Hovering over any children of the container is the same as hovering over the controls. The exception is where the player and the child share a common ancestor (that's not the fullscreen container) ... sounds complex but it's not. You can also gain pretty fine control this way with pointer events.

Under the hood: it adds a `utils/elements/closest` helper method to find the right ancestor. If found this is returned as the fullscreen target in place of the player container. Fullscreen is instantiated slightly earlier in the setup so this container is available for the `listeners.controls` call. In here we add some more 'mouseenter/mouseleave' listeners to any direct descendants of the container, that aren't also ancestors of the player. And that's it. No extra classes, nothing else. There are some style changes to the demo (top margin on the player) but these would be project specific.

Thanks for reading.
2020-04-04 13:43:51 +10:00
Som Meaden 44ef0bbc87 include npm run serve shortcut for gulp serve (useful where gulp isn't installed globally) 2020-04-04 13:02:17 +10:00
Sam Potts 8f5b59c18c Sentry upgrade for demo 2020-03-30 23:39:01 +11:00
Sam Potts d06881783d Formatting fixes 2020-03-30 17:04:43 +11:00
Sam Potts da943b384c Merge branch 'develop' into css-variables
# Conflicts:
#	demo/dist/demo.css
#	demo/dist/demo.min.js.map
#	demo/index.html
#	dist/plyr.css
#	dist/plyr.min.js.map
#	dist/plyr.min.mjs.map
#	dist/plyr.polyfilled.min.js.map
#	dist/plyr.polyfilled.min.mjs.map
#	gulpfile.js
#	src/sass/base.scss
#	src/sass/components/control.scss
#	src/sass/settings/colors.scss
#	src/sass/settings/controls.scss
2020-03-30 10:45:57 +11:00
Sam Potts ad63af5096 Added prettier script 2020-03-29 12:13:24 +11:00
Sam Potts 09598f07bf Merge branch 'develop' of github.com:sampotts/plyr into develop
# Conflicts:
#	package.json
#	yarn.lock
2020-03-29 12:02:59 +11:00
Sam Potts ef7b30c1b8 Package upgrades 2020-03-29 11:36:51 +11:00
Sam Potts 5516db22c3 Gulp file broken down 2020-03-29 11:36:44 +11:00
Sam Potts 155add66bd Merge pull request #1686 from lawchihon/master
Added missing full screen options for type definition
2020-03-29 11:22:51 +11:00
Sam Potts 39558cbabc Merge pull request #1731 from nisarhassan12/master
update the gitpod setup description to be more precise.
2020-03-29 11:22:20 +11:00
Sam Potts 4935c92b63 Merge pull request #1697 from hug963/fix-vimeo-playback-rate
Fix vimeo playback rate
2020-03-29 11:21:01 +11:00
Sam Potts 66f1f28646 Merge pull request #1724 from Steejo/ads-plugin-fixes
Ads plugin fixes to allow multiple VAST requests
2020-03-29 11:20:17 +11:00
Sam Potts 48758bd5f0 Merge pull request #1705 from doublex/master
preview-thumbnails via src:callback()
2020-03-29 11:19:38 +11:00
Sam Potts 2f26c80c88 Merge pull request #1739 from ydylla/ignore-internal-play-promises
Ignore internal play promises
2020-03-29 11:18:08 +11:00
Sam Potts be3ffc1f96 Merge pull request #1727 from jnoordsij/fix_shadowroot
Fix shadowroot
2020-03-29 11:17:33 +11:00
Sam Potts 3509995756 Build 2020-03-27 23:50:40 +11:00
Sam Potts 88dc981f4b Package upgrades 2020-03-27 23:50:36 +11:00
Sam Potts a65acbe43f Merge branch 'master' into develop 2020-03-27 23:37:57 +11:00
ydylla 6a1d6f13a2 add promise footnote to togglePlay 2020-03-23 22:56:37 +01:00
ydylla 71928443f3 silence all internal play promises 2020-03-23 22:56:32 +01:00
Nisar Hassan Naqvi 10f366fe32 update the gitpod setup description to be more precise. 2020-03-16 07:16:22 +00:00
Sam Potts fcd82088a5 Update readme.md 2020-03-14 12:46:33 +11:00
Sam Potts c9b6d9685c Update readme.md 2020-03-14 12:46:05 +11:00
Sam Potts 3069d77b74 Merge pull request #1729 from opencollective/opencollective
Add financial contributors for Open Collective
2020-03-14 12:45:17 +11:00
jess b7367d8275 Add financial contributors for Open Collective
Hi, I'm making updates for Open Collective. Either you or another core contributor signed this repository up for Open Collective. This pull request adds financial contributors from your Open Collective https://opencollective.com/plyr ❤️

  What it does:
  - adds a badge to show the latest number of financial contributors
  - adds a banner displaying contributors to the project on GitHub
  - adds a banner displaying all individuals contributing financially on Open Collective
  - adds a section displaying all organizations contributing financially on Open Collective, with their logo and a link to their website

P.S: As with any pull request, feel free to comment or suggest changes.

  Thank you for your great contribution to the Open Source community. You are awesome! 🙌
  And welcome to the Open Collective community! 😊

  Come chat with us in the #opensource channel on https://slack.opencollective.com - great place to ask questions and share best practices with other Open Source sustainers!
2020-03-13 18:25:13 -07:00
Jesper 99ae4eb3c5 Compare fullscreenElement with shadowroot host if player is in shadow DOM 2020-03-10 09:30:42 +01:00
Jesper c7bf0c5c03 Fix prototype used for selector matcher function 2020-03-10 09:19:34 +01:00
Steejo fd353225c2 Ads plugin fixes to allow multiple VAST requests 2020-03-09 23:18:19 +00:00
max ace682abbd Fixes2 2020-02-26 10:41:26 +01:00
max b212b25a9e Fixes 2020-02-26 10:35:08 +01:00
Sam Potts 3c127afeb9 Merge pull request #1706 from sampotts/master
Merge back
2020-02-26 12:33:37 +11:00
Sam Potts 84eef1d747 Merge pull request #1701 from mogzol/patch-1
Use number instead of string in TS quality definitions
2020-02-26 12:33:07 +11:00
Sam Potts 8a0086397f Merge pull request #1704 from hug963/add-missing-ts-types
Add missing Typescripts types and options
2020-02-26 12:32:08 +11:00
Sam Potts 75327f2242 Merge pull request #1703 from LeBenLeBen/develop
Completely hide SVG icons to screen readers
2020-02-26 12:31:34 +11:00
max 81b41be750 preview-thumbnails via src:callback() 2020-02-25 17:53:44 +01:00
Hugues 6020f95e50 Add missing Typescripts types and options 2020-02-25 11:10:06 +00:00
Benoît Burgener bc8a25d0da Completely hide SVG icons to screen readers
SVG icons should be ignored by screen readers since they have complimentary labels (aria-label or plyr__sr-only). The current « presentation » role simply makes the element behave like a « span » which is incorrect, aria-hidden prevents screen readers from taking care of these elements at all.
2020-02-25 10:46:31 +01:00
Sam Potts 29e62a1e4f Merge pull request #1702 from sampotts/master
Merge back
2020-02-25 09:21:31 +11:00
Sam Potts b12ec094c5 Update contributing.md 2020-02-25 09:20:40 +11:00
Morgan Zolob fea5e76b76 Use number instead of string in TS quality definitions
Using strings for the quality doesn't work, plyr expects numbers, so this fixes the definitions.
2020-02-24 11:33:50 -08:00
Hugues 0cf5d25a7f catch error in setPlaybackRate on Vimeo 2020-02-20 12:57:47 +00:00
Sam Potts 977a8393f8 Merge pull request #1695 from CzBiX/patch-2
Fix issue when controls config is string or element
2020-02-19 08:57:26 +11:00
Sam Potts 302fd93e86 Merge pull request #1694 from nisarhassan12/master
simplify contributions by fully automating the dev setup with gitpod
2020-02-19 08:55:50 +11:00
CzBiX 70470ae8d2 Fix issue when controls config is string or element 2020-02-17 18:02:39 +08:00
Nisar Hassan Naqvi 425b39a762 simplify code contributions by fully automating the dev setup with gitpod 2020-02-17 04:21:35 +00:00
Sam Potts 206e3b57d1 v3.5.10 2020-02-14 17:37:32 +00:00
Sam Potts a34bc3ef29 Merge pull request #1692 from sampotts/develop
v3.5.10
2020-02-14 17:33:41 +00:00
Sam Potts 6350b7b9e4 v3.5.10
- iOS volume display fix
2020-02-14 17:33:09 +00:00
Sam Potts 04d06f2242 v3.5.9 deployed 2020-02-14 17:00:33 +00:00
Sam Potts 924049aa14 Merge pull request #1691 from sampotts/develop
v3.5.9
2020-02-14 16:55:56 +00:00
Sam Potts 442427ebd5 v3.5.9
-   Fix for regression with volume control width
-   Ensure poster image is not downloaded again for HTML5 videos
2020-02-14 16:54:22 +00:00
Sam Potts 7954c92c0b Merge branch 'master' into develop 2020-02-14 16:53:55 +00:00
Sam Potts 5afb14283a Fix for regression with volume control width 2020-02-14 16:53:31 +00:00
Sam Potts bfc541b880 Ensure poster image is not downloaded again for HTML5 videos 2020-02-14 16:53:23 +00:00
Sam Potts 426280f90c Merge branch 'master' into develop 2020-02-13 15:06:57 +00:00
Sam Potts 2e2c5ad72a Styles 2020-02-13 15:06:38 +00:00
Sam Potts ecb091af6b Fix syntax with funding.yml 2020-02-13 12:40:08 +00:00
Sam Potts ecb882b719 Added Open Collective as funding option 2020-02-13 12:39:30 +00:00
Sam Potts cddd9c30db More styles clean up 2020-02-12 14:36:30 +00:00
Sam Potts f6a4625495 Removed redundant keys property 2020-02-12 11:35:47 +00:00
John Law 550bd543e3 Added missing full screen options for type definition 2020-02-12 00:06:28 -08:00
Sam Potts a6ff0274a9 v3.5.8 deployed 2020-02-10 18:38:54 +00:00
Sam Potts 841746210a Merge pull request #1684 from sampotts/develop
v3.5.8
2020-02-10 18:35:42 +00:00
Sam Potts 156abda66a Changelog update 2020-02-10 18:34:26 +00:00
Sam Potts 1619510dcf Speed settings logic improvements 2020-02-10 18:34:05 +00:00
Sam Potts ff8dedd4ec Menu border color tweak 2020-02-10 17:53:23 +00:00
Sam Potts 637823c8ce Started on v3.5.8 changelog 2020-02-10 11:31:39 +00:00
Sam Potts 01219be817 Comment clean up 2020-02-10 11:31:18 +00:00
Sam Potts 59e3afba03 Merge branch 'develop' of github.com:sampotts/plyr into develop 2020-02-10 11:28:59 +00:00
Sam Potts db05322ba2 Merge pull request #1670 from ydylla/previewThumbnails-setter
Add previewThumbnails source setter
2020-02-10 11:28:20 +00:00
Sam Potts 74e5c78b3f Merge pull request #1678 from ydylla/fix-thumb-size-per-css
Improve thumbnail size calculations when size is set per css
2020-02-10 11:26:53 +00:00
Sam Potts ea350f9d1d Update demo video 2020-02-10 11:25:48 +00:00
Sam Potts eda192639d Demo packages updated 2020-02-10 11:25:39 +00:00
Sam Potts 0f16a018ff Set referrerPolicy in the demo 2020-02-10 11:25:25 +00:00
Sam Potts 7ca74f48bc Added vimeo options to hide controls and set referrerPolicy 2020-02-10 11:24:38 +00:00
Sam Potts 5837c2d5f0 SASS orginasation clean up and flex-direction added 2020-02-10 11:23:57 +00:00
Sam Potts e50b35d195 Merge pull request #1554 from sampotts/dependabot/npm_and_yarn/mixin-deep-1.3.2
Bump mixin-deep from 1.3.1 to 1.3.2
2020-02-09 22:17:19 +00:00
Sam Potts 433b729c45 Merge pull request #1575 from sampotts/dependabot/npm_and_yarn/lodash.merge-4.6.2
Bump lodash.merge from 4.6.1 to 4.6.2
2020-02-09 22:17:10 +00:00
Sam Potts bb7f7d5e2a 3.5.7 2020-02-09 21:59:40 +00:00
Sam Potts 8c44425665 Merge pull request #1679 from sampotts/develop
3.5.7
2020-02-09 21:53:24 +00:00
Sam Potts 93e3f8946a Docs updates 2020-02-09 21:52:34 +00:00
Sam Potts 95431639a0 Merge branch 'develop' of github.com:sampotts/plyr into develop 2020-02-09 21:42:54 +00:00
Sam Potts 3e3186cfeb Update docs for speed options tweaks 2020-02-09 21:42:27 +00:00
Sam Potts 2d13ad3d39 Focus trap improvements 2020-02-09 21:42:12 +00:00
ydylla d7d0f5fca0 add previewThumbnails to source setter docs 2020-02-09 17:03:31 +01:00
Sam Potts 74ba6a96fc Set download attribute for HTML5 only 2020-02-09 10:36:32 +00:00
Sam Potts e1cb2f24f5 Merge pull request #1490 from antonyoneill/develop
Prevent default on settings control click
2020-02-09 10:30:33 +00:00
Sam Potts 59e3ef7248 Comments 2020-02-09 10:20:40 +00:00
Sam Potts 0b1c480729 Package upgrades 2020-02-08 23:09:50 +00:00
Sam Potts 90dc985657 Clean up speed options logic 2020-02-08 23:09:41 +00:00
Sam Potts b5456e1de7 Merge pull request #1671 from ydylla/improve-speed-options
Use the configured speed options
2020-02-08 22:29:49 +00:00
Sam Potts 976eebc2a2 Merge pull request #1672 from ydylla/improve-quality-change
Improve/fix quality change state restoring
2020-02-08 22:19:44 +00:00
Sam Potts f00c279366 Merge pull request #1675 from Laerdal/focus-trap-only-fullscreen
Trap keyboard focus only when fullscreen
2020-02-08 22:18:59 +00:00
Sam Potts b651d6f027 Merge pull request #1676 from Code1110/develop
Add download attribute to download button
2020-02-08 22:18:14 +00:00
Sam Potts b63b62f6dc Merge pull request #1677 from ydylla/fix-scrubbing-chrome-android
Fix preview thumbnail scrubbing not working on mobile touch devices
2020-02-08 22:17:41 +00:00
Sam Potts 0f08c7c13a Ignore quality change if it matches existing 2020-02-08 21:48:51 +00:00
ydylla 97f8093a8d allows to set only width or height for thumb css size
Also fixes sprites when css thumb size is used
2020-02-08 19:15:02 +01:00
ydylla 9075ea189a fix scrubbing for chrome android & hide thumb preview on touchend
Chrome android sends TouchEvent which does not have a button property.
2020-02-08 19:14:06 +01:00
Code1110 c33f0995f9 add download attribute to download button 2020-02-07 18:15:56 +01:00
Kimberley Jensen e17da7dfd4 Bail out of focus trap if fullscreen is not active
- detailed in https://github.com/sampotts/plyr/issues/1665
2020-02-07 15:00:04 +01:00
ydylla 299f712cc9 actually use the configured speed options 2020-02-01 16:37:38 +01:00
ydylla f755a3c401 preserve playback rate at quality change 2020-02-01 16:35:24 +01:00
ydylla 472bb479d4 fix regression: not restoring playback state after quality change 2020-02-01 16:35:17 +01:00
ydylla 61a24eab76 add previewThumbnails source setter #1369 2020-02-01 16:32:19 +01:00
Sam Potts 8b9521d5a5 Fix beta deployment 2020-01-30 14:57:14 +00:00
Sam Potts 818e1efd43 Deployed 3.5.7-beta.0 2020-01-30 14:56:52 +00:00
Sam Potts 58f5380694 Merge pull request #1662 from sampotts/develop
3.5.7
2020-01-30 14:23:40 +00:00
Sam Potts 9d51291125 Merge pull request #1663 from sampotts/master
Merge back to beta
2020-01-30 14:23:10 +00:00
Sam Potts fefcca7805 Prepare for 3.5.7 release 2020-01-30 11:34:07 +00:00
Sam Potts 5204f33d45 Package upgrades 2020-01-30 11:07:14 +00:00
Sam Potts 0041ea3050 Merge branch 'develop' of github.com:sampotts/plyr into develop 2020-01-26 22:05:29 +00:00
Sam Potts f8a28b632c Audio style fix 2020-01-26 22:05:21 +00:00
Sam Potts 9c817cff68 Merge pull request #1653 from thatrobotdev/patch-2
Patch 2
2020-01-22 11:32:37 +00:00
Sam Potts 521e8abf88 Merge pull request #1654 from thatrobotdev/patch-3
Fix broken link to new plugin
2020-01-22 11:31:02 +00:00
Sam Potts 9fe03c7474 Merge pull request #1655 from laukstein/patch-1
Uncaught RangeError: Maximum call stack size exceeded
2020-01-22 11:30:19 +00:00
Binyamin Laukstein c3f10e7df3 Uncaught RangeError: Maximum call stack size exceeded
Fix formatTime infinite loop #1621
2020-01-22 08:53:08 +02:00
James Kerrane 1b8c51f45e Fix broken link to new plugin
Fix broken link, and update to an active project replacement https://github.com/chintan9/plyr-react.
2020-01-21 23:15:18 -07:00
James Kerrane 1bb4c207f1 Change vimeo demo video
Change vimeo video to a more general video, fix #1626.
2020-01-21 23:08:55 -07:00
Sam Potts b6da2702a2 Fix reference 2020-01-21 22:30:58 +00:00
Sam Potts fb704b945d Presentation fixes 2020-01-21 22:29:00 +00:00
Sam Potts 71d6f59d56 HTML5 poster fixes for multiple downloads 2020-01-21 22:28:48 +00:00
Sam Potts 89136bc2e6 Merge branch 'develop' of github.com:sampotts/plyr into develop
# Conflicts:
#	src/sass/components/video.scss
2020-01-21 22:15:38 +00:00
Sam Potts b6d94e000f Merge pull request #1651 from shravan2x/develop
Fixed Plyr container not resizing responsively
2020-01-21 22:11:32 +00:00
Shravan Rajinikanth bfcb7133cb Fixed Plyr container not resizing responsively 2020-01-19 06:05:12 -08:00
Sam Potts 7883792ccc Fix issue with browser sync preview 2020-01-14 22:33:01 +00:00
Sam Potts def3668030 Fix issues with fixed ratios and 100% height/width 2020-01-14 22:32:45 +00:00
Sam Potts a77d2d56f6 Fix build 2020-01-14 07:50:48 +00:00
Sam Potts 7ee041403f Merge branch 'develop' of github.com:sampotts/plyr into develop
# Conflicts:
#	src/js/controls.js
#	src/sass/components/volume.scss
2020-01-14 07:46:48 +00:00
Sam Potts 53a400027f Remove logic to hide/show volume controls based on audio track 2020-01-14 07:45:24 +00:00
Sam Potts a2498acf7c Manually merge PR #1629 2020-01-14 07:44:59 +00:00
Sam Potts 28a1098c0f Merge pull request #1629 from sumanbh/ios-show-volume
IOS - Fix being unable to unmute auto-played videos
2020-01-14 07:31:34 +00:00
Sam Potts 6ffaef35cf Manually merged PR #1607 2020-01-14 07:25:41 +00:00
Sam Potts ff105ee203 Fix browser sync vs watch issues 2020-01-14 07:25:04 +00:00
Sam Potts 56c0d7bd4d Fix linting issues 2020-01-13 16:38:12 +00:00
Sam Potts d00d31961e Merge pull request #1484 from MaxGiting/patch-1
Improve clarity
2020-01-13 16:32:43 +00:00
Sam Potts b2d3ef5f38 Merge pull request #1505 from nskazki/detach-event-listeners-on-destroy
Detach event listeners on destroy
2020-01-13 16:31:17 +00:00
Sam Potts b2ac730572 Merge pull request #1535 from skerbis/master
adds: REDAXO CMS Plyr AddOn
2020-01-13 16:29:41 +00:00
Sam Potts 3424d08d3a Merge pull request #1521 from ondratra/develop
typescript typings
2020-01-13 16:29:15 +00:00
Sam Potts 5dd9462bed Merge pull request #1516 from azizhk/toggle_return_promsie
Toggle also returns promise
2020-01-13 16:25:17 +00:00
Sam Potts 402eb2b761 Merge pull request #1552 from SoftCreatR/patch-2
Fix ads configuration
2020-01-13 16:24:25 +00:00
Sam Potts 63d74eee68 Merge pull request #1560 from 0xflotus/develop
Update readme.md
2020-01-13 16:23:39 +00:00
Sam Potts 166a27d094 Comment clean up 2020-01-13 16:22:49 +00:00
Sam Potts 4f06e2eb71 Merge pull request #1570 from felipedeboni/ie11-resetonend-fix
Prevents IE11 with resetOnEnd option set to true to play video again
2020-01-13 16:21:46 +00:00
Sam Potts 0b240ae7d1 Fix linting issues 2020-01-13 16:20:02 +00:00
Sam Potts 6b0e5cd6f1 Merge branch 'master' into develop 2020-01-13 16:18:38 +00:00
Sam Potts 2463434d27 Merge branch 'develop' of github.com:sampotts/plyr into develop 2020-01-13 16:18:24 +00:00
Sam Potts c09b9ac01c Manually port over change from PR #1616 2020-01-13 16:18:05 +00:00
Sam Potts 15a1cdde89 Merge pull request #1565 from lmislm/patch-1
Update defaults.js
2020-01-13 16:16:35 +00:00
Sam Potts 3a1da5ad36 Merge pull request #1525 from lunika/play-button-aria
️(controls) change play button aria-label value when its state change
2020-01-13 16:09:04 +00:00
Sam Potts 67cb324aed Merge pull request #1577 from avidnewmedia/issue-615
#615: updates to vimeo and youtube buffering state
2020-01-13 16:06:27 +00:00
Sam Potts 8736fa8a52 Manually port over change from PR #1625 2020-01-13 16:04:27 +00:00
Sam Potts cb4dab4250 Merge pull request #1579 from taion/fix-listener-return
fix: Fix handling listener return value
2020-01-13 15:59:36 +00:00
Sam Potts c56916a8e0 Merge pull request #1582 from bseib/1306-preserve-svg-symbol-viewbox
Preserve viewBox attribute in SVG sprite symbols
2020-01-13 15:58:27 +00:00
Sam Potts 93963d3915 Merge pull request #1630 from pjbaert/patch-1
Typo in readme
2020-01-13 15:56:27 +00:00
Sam Potts e7c6f965b4 Merge branch 'master' into develop 2020-01-13 15:49:47 +00:00
Sam Potts 4f263ebb1a Added local server, package upgrades 2020-01-13 15:49:29 +00:00
Sam Potts c15bdabf0c Update readme.md 2020-01-12 21:35:40 +00:00
Pieter-Jan Baert 80a077c50a Typo in readme 2019-12-06 16:00:48 +01:00
Suman Bhattarai 42d72c5303 fix being unable to unmute autoplayed video on IOS 2019-12-03 10:34:27 -07:00
Sam Potts 74e3990604 Merge pull request #1590 from ayunami2000/patch-1
typo
2019-10-23 13:01:56 +11:00
ayunami2000 e6e391ad6a typo
change s to z in "Customisable" (now "Customizable")
2019-10-14 15:59:01 -04:00
Broc Seib 12e5099c92 Preserve viewBox attribute in SVG sprite symbols
When generating the SVG sprite (using imagemin and svstore) the imagemin
step needs to NOT cleanup/remove the viewBox attributes from the
individual svg files.

fixes #1306
2019-10-01 17:34:49 -04:00
Jimmy Jia 72afffbc8d fix: Fix handling listener return value 2019-09-25 14:21:13 -04:00
Dustin Harrell 627df20b6d #615: updates to vimeo and youtube plugins to ensure that loading classes are added as content is buffering 2019-09-23 18:26:49 -04:00
dependabot[bot] 640bc99661 Bump lodash.merge from 4.6.1 to 4.6.2
Bumps [lodash.merge](https://github.com/lodash/lodash) from 4.6.1 to 4.6.2.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2019-09-22 15:26:12 +00:00
Felipe K. De Boni bb43ef15fe Prevents IE11 with resetOnEnd option set to true to play video again 2019-09-16 11:53:07 -03:00
Baskerville* 7907652bc9 Update defaults.js
update defaults.i18n
2019-09-06 16:18:40 +08:00
0xflotus dce665b792 Update readme.md 2019-09-01 18:39:37 +02:00
dependabot[bot] 32bf684b17 Bump mixin-deep from 1.3.1 to 1.3.2
Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/jonschlinkert/mixin-deep/releases)
- [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-28 22:38:57 +00:00
Sascha Greuel e978bc8690 Fixed ads configuration 2019-08-27 19:28:51 +02:00
Thomas Skerbis 91503d3698 Update readme.md 2019-08-15 21:55:22 +02:00
Thomas Skerbis 1721f6e9e2 Update readme.md 2019-08-15 21:53:41 +02:00
Manuel Raynaud c7b5aa9197 ️(controls) change play button aria-label value when its state change
The aria-label attribute set on all play buttons does not change
according the player state. When the video is playing, the aria-label
should change to pause otherwise screen reader will not detect that this
button now can be used to pause the video.
2019-08-07 18:00:02 +02:00
ondratra 800c8e0a17 typescript typings 2019-08-07 00:08:10 +02:00
Aziz Khambati d771da9abf Toggle also returns promise 2019-07-30 18:19:10 +05:30
nskazki b36b92b247 Detach event listeners on destroy
if "on" receives "this", it attaches "eventListeners" array to "this" and stores "{ element, type, callback }" hash in there.
later, during the destruction process, all the entries from this array will be processed by "unbindListeners" helper to detach all the event listeners.
2019-07-19 16:26:01 +03:00
Sam Potts aa51719a55 Merge pull request #1497 from ffpetrovic/patch-1
Update index.html
2019-07-12 08:46:42 +10:00
Filip Petrovic a28685536a Update index.html 2019-07-11 23:18:35 +02:00
Antony O'Neill 400fd77d0a Prevent default on settings icon click 2019-07-04 19:02:22 +01:00
MaxGiting a2bf974058 Improve clarity 2019-07-01 15:09:29 +01:00
Sam Potts 7c442c9357 3.5.6 2019-06-21 12:35:47 +10:00
Sam Potts e17e0a81dd Package upgrade 2019-06-21 12:35:22 +10:00
Sam Potts 2488299d7b Edge fix 2019-06-21 12:34:49 +10:00
Sam Potts dfc09b8e04 v3.5.5 deployed 2019-06-21 00:24:28 +10:00
Sam Potts 6438baaddc Merge branch 'develop' 2019-06-21 00:19:51 +10:00
Sam Potts 8fc6c2ba52 File rename and clean up 2019-06-21 00:19:37 +10:00
Sam Potts 95092edc93 Merge pull request #1472 from sampotts/develop
v3.5.5
2019-06-21 00:12:10 +10:00
Sam Potts c4b3e0672e Clean up 2019-06-21 00:10:57 +10:00
Sam Potts e8e2b8ba39 Merge branch 'master' into develop
# Conflicts:
#	.eslintrc
#	demo/dist/demo.css
#	demo/dist/demo.js
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.css
#	dist/plyr.js
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.min.mjs
#	dist/plyr.min.mjs.map
#	dist/plyr.mjs
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	dist/plyr.polyfilled.min.mjs
#	dist/plyr.polyfilled.min.mjs.map
#	dist/plyr.polyfilled.mjs
#	package.json
#	readme.md
#	src/js/listeners.js
#	yarn.lock
2019-06-20 23:56:19 +10:00
Sam Potts 2e40b91ec1 Styling tweaks for demo 2019-06-20 23:50:46 +10:00
Sam Potts 97d9228bed Aspect ratio tweaks 2019-06-03 20:13:16 +10:00
Sam Potts 0f14865d56 Add duration (commented out) in defaults 2019-06-03 20:12:43 +10:00
Sam Potts c94ab2a39f Repaint clean up 2019-06-03 20:12:21 +10:00
Sam Potts ab89e055de Demo tweaks 2019-06-03 20:11:31 +10:00
Sam Potts ac6e3dba5a Fix for thumbnails in demo for audio 2019-06-03 00:28:09 +10:00
Sam Potts d9d2c4a219 Demo tweaks 2019-06-03 00:26:08 +10:00
Sam Potts 1890a9378d Gulp tweaks 2019-06-02 23:16:45 +10:00
Sam Potts ac88e6e190 Demo clean up 2019-06-02 23:16:29 +10:00
Sam Potts 15cbae8a19 Removed commented out code for Edge 2019-06-02 22:25:44 +10:00
Sam Potts aaf096d96d Removed raven-js from dependencies 2019-06-02 22:25:17 +10:00
Sam Potts 93f5acbffd Fixed cite display 2019-06-02 22:25:00 +10:00
Sam Potts 9c717275d2 Packages for demo separated 2019-06-02 22:24:41 +10:00
Sam Potts 50a7c2fad6 Build 2019-06-01 20:01:24 +10:00
Sam Potts 95be6e721b Fixed merge 2019-06-01 20:01:19 +10:00
Sam Potts dc2e012cc9 Clean up 2019-06-01 20:01:09 +10:00
Sam Potts 12c6282d14 Merge branch 'develop' into css-variables
# Conflicts:
#	.eslintrc
#	demo/dist/demo.css
#	demo/dist/demo.js
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.css
#	dist/plyr.js
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.min.mjs
#	dist/plyr.min.mjs.map
#	dist/plyr.mjs
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	dist/plyr.polyfilled.min.mjs
#	dist/plyr.polyfilled.min.mjs.map
#	dist/plyr.polyfilled.mjs
#	gulpfile.js
#	package.json
2019-06-01 19:55:14 +10:00
Sam Potts 0249772f01 Clean up 2019-06-01 19:50:29 +10:00
Sam Potts 5d699d5f47 Merge branch 'develop' of github.com:sampotts/plyr into develop 2019-06-01 19:29:21 +10:00
Sam Potts 34d79a5443 Merge pull request #1453 from aFarkas/develop
youtube multiple small issues
2019-06-01 18:46:56 +10:00
Sam Potts 1e761e237a Merge pull request #1458 from Jason-Cooke/patch-1
docs: fix typo
2019-06-01 18:46:14 +10:00
Sam Potts 36ad132c82 Edge fix 2019-06-01 18:45:12 +10:00
Sam Potts c9055f391b Linting changes 2019-06-01 18:45:07 +10:00
Jason Cooke 99c95363b4 docs: fix typo 2019-06-01 11:13:44 +12:00
Sam Potts c90526bf07 Create FUNDING.yml 2019-05-27 17:17:05 +10:00
Alexander Farkas 97a6e72e10 fix youtube embed + handle early destroy 2019-05-24 16:55:45 +02:00
Alexander Farkas f100caba81 fix youtube embed + handle early destroy 2019-05-24 16:53:58 +02:00
Sam Potts 80aa6ffe43 Linting changes 2019-04-30 23:44:05 +10:00
Sam Potts 0694e58650 v3.5.4 2019-04-25 12:14:48 +10:00
Sam Potts e644eeb5b6 Merge pull request #1423 from sampotts/develop
v3.5.4
2019-04-25 12:11:06 +10:00
Sam Potts 5ddd9e02de Fix for the menu in the shadow DOM 2019-04-25 12:09:46 +10:00
Sam Potts a064f0b4fd Merge branch 'develop' of github.com:sampotts/plyr into develop 2019-04-25 12:04:38 +10:00
Sam Potts b9dbcc30fa Eslint config rename 2019-04-25 12:03:42 +10:00
Sam Potts 7ab34c7d2e Changelog 2019-04-25 12:03:32 +10:00
Sam Potts 17caa3c57b Fix merging class 2019-04-25 12:03:24 +10:00
Sam Potts 2e6361898b Package updates 2019-04-25 12:03:13 +10:00
Sam Potts a1b2c0419f Ads improvements for volume and race condition fix 2019-04-25 12:03:04 +10:00
Sam Potts 9b81e776fb Styling for control changes 2019-04-25 12:02:48 +10:00
Sam Potts 7a4a7dece8 Ratio improvements 2019-04-25 12:02:37 +10:00
Sam Potts f0d3e8c3b9 Fix for className being wiped out 2019-04-25 12:01:52 +10:00
Sam Potts ad68d9484f Clean up and API change 2019-04-25 12:01:30 +10:00
Sam Potts c3fd822857 Notes on autoplay support 2019-04-25 12:01:15 +10:00
Sam Potts e147f3a754 Formatting 2019-04-25 12:01:00 +10:00
Sam Potts b694e7d3ab Use polyfill.io v3 2019-04-22 12:28:26 +10:00
Sam Potts b2fff4c33f Increase speed limits 2019-04-15 22:08:09 +10:00
Sam Potts 9c1060d9b0 Merge pull request #1416 from emielbeinema/fix_webcomponents
Make menu work in WebComponent
2019-04-15 21:41:23 +10:00
Emiel Beinema a91652287b Don't close menu on click in menu in webcomponent 2019-04-15 12:17:10 +02:00
Emiel Beinema 2cf44c236d Use querySelector on container for showMenuPanel 2019-04-15 12:16:48 +02:00
Sam Potts 243db9eda3 Fix issue with empty controls and preview thumbs 2019-04-13 12:56:15 +10:00
Sam Potts b675ba1f35 Fix issue with setGutter call 2019-04-13 12:55:48 +10:00
Sam Potts e9367ee85e Fix setting initial speed (fixes #1408) 2019-04-12 20:18:17 +10:00
Sam Potts d9b7928ce6 Merge branch 'master' into develop 2019-04-12 19:00:23 +10:00
Sam Potts cdacae6697 Set download URL via setter 2019-04-12 19:00:17 +10:00
Sam Potts 2bd08cdc28 3.5.3 2019-04-12 18:44:05 +10:00
Sam Potts 5fefabe3bd Merge pull request #1410 from sampotts/develop
v3.5.3
2019-04-12 18:39:14 +10:00
Sam Potts e281078441 Bump version 2019-04-12 18:38:46 +10:00
Sam Potts 9bb75f6f52 Changelog 2019-04-12 18:37:11 +10:00
Sam Potts 3b7a24456d Package upgrades 2019-04-12 18:36:55 +10:00
Sam Potts c885d59270 Moved all video styles together 2019-04-12 12:43:45 +10:00
Sam Potts 9f30d54963 Merge branch 'master' into develop
# Conflicts:
#	readme.md
2019-04-12 12:40:12 +10:00
Sam Potts b247093495 Aspect ratio improvements (fixes #1042, fixes #1366) 2019-04-12 12:19:48 +10:00
Sam Potts 9ca7b861a9 Autoplay tweak for HTML5 2019-04-12 12:14:12 +10:00
Sam Potts 2eccf0dd05 Fix YouTube autoplay (fixes #1185) 2019-04-12 12:13:46 +10:00
Sam Potts 566b957e65 Merge pull request #1362 from doublex/master
#46 - two patches from 'jamesoflol'
2019-04-11 21:20:23 +10:00
Sam Potts a8456f4ca7 Merge pull request #1404 from taion/clear-timeouts
fix: Properly clear all timeouts on destroy
2019-04-11 21:18:52 +10:00
Sam Potts 0f3098040d Merge pull request #1407 from freezer278/http-youtube-fix
fixed setting youtube host for non-https case
2019-04-11 21:18:15 +10:00
Sam Potts 996075decc More work on variable usage 2019-04-11 20:50:20 +10:00
Vladimir Morozov 21539be3f2 code cleanup 2019-04-04 09:32:38 +03:00
Vladimir Morozov c22f5c4b39 code cleanup 2019-04-04 08:56:46 +03:00
Vladimir Morozov f4b47a9275 fixed setting youtube host for non-https case 2019-04-04 08:51:20 +03:00
Jimmy Jia 266b70d9d0 fix: Properly clear all timeouts on destroy 2019-04-01 14:42:51 -04:00
Sam Potts 6e68ad6d15 Update readme.md 2019-03-26 21:43:59 +11:00
Sam Potts 848e798809 Fix merge 2019-03-16 12:15:20 +11:00
Sam Potts 35f7ee9c59 Merge branch 'develop' into css-variables
# Conflicts:
#	demo/dist/demo.css
#	demo/index.html
#	dist/plyr.css
#	gulpfile.js
#	package.json
#	yarn.lock
2019-03-16 12:14:20 +11:00
Sam Potts c202551e6d Merge branch 'develop' of github.com:sampotts/plyr into develop 2019-03-16 11:58:12 +11:00
Sam Potts 5b7a025d26 Housekeeping 2019-03-16 11:57:15 +11:00
Sam Potts 26bcf83960 Merge pull request #1376 from ar31an/patch-1
Update poster src
2019-03-07 18:59:58 +11:00
Arslan Javed e4acff4f8d Update poster src 2019-03-07 12:54:07 +05:00
Sam Potts 568ddf2390 Merge pull request #1375 from sampotts/master
Merge back
2019-03-07 18:30:05 +11:00
Your Name ce91945544 Preview seek: optional hours and ms in VTT parser 2019-02-27 15:45:24 +01:00
Your Name 11fed8d1b5 Preview seek: fix: allow absolute thumbnail paths 2019-02-27 15:43:36 +01:00
Sam Potts bdd513635f Work on outline/focus styles 2018-12-08 17:06:20 +11:00
Sam Potts b7b2e3c0aa Merge branch 'develop' into css-variables
# Conflicts:
#	demo/dist/demo.css
#	demo/dist/demo.js
#	demo/dist/demo.js.map
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.css
#	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
#	gulpfile.js
#	src/sass/components/captions.scss
#	src/sass/components/control.scss
2018-11-11 11:05:09 +11:00
Sam Potts 3e0a911418 WIP 2018-05-26 13:37:10 +10:00
159 changed files with 57946 additions and 30318 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
indent_size = 4 indent_size = 2
indent_style = space indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
+30
View File
@@ -0,0 +1,30 @@
{
"parser": "babel-eslint",
"extends": ["airbnb-base", "prettier"],
"plugins": ["simple-import-sort", "import"],
"env": {
"browser": true,
"es6": true
},
"globals": {
"Plyr": false,
"jQuery": false
},
"rules": {
"import/no-cycle": "warn",
"padding-line-between-statements": [
"error",
{
"blankLine": "never",
"prev": ["singleline-const", "singleline-let", "singleline-var"],
"next": ["singleline-const", "singleline-let", "singleline-var"]
}
],
"sort-imports": "off",
"import/order": "off",
"simple-import-sort/sort": "error"
},
"parserOptions": {
"sourceType": "module"
}
}
-35
View File
@@ -1,35 +0,0 @@
{
"parser": "babel-eslint",
"extends": ["airbnb-base", "prettier"],
"env": {
"browser": true,
"es6": true
},
"globals": {
"Plyr": false,
"jQuery": false
},
"rules": {
"import/no-cycle": 1,
"no-const-assign": 1,
"no-shadow": 0,
"no-this-before-super": 1,
"no-undef": 1,
"no-unreachable": 1,
"no-unused-vars": 1,
"constructor-super": 1,
"valid-typeof": 1,
"indent": [2, 4, { "SwitchCase": 1 }],
"quotes": [2, "single", "avoid-escape"],
"semi": [2, "always"],
"eqeqeq": [2, "always"],
"one-var": [2, "never"],
"comma-dangle": [2, "always-multiline"],
"spaced-comment": [2, "always"],
"no-restricted-globals": 2,
"no-param-reassign": [2, { "props": false }]
},
"parserOptions": {
"sourceType": "module"
}
}
+5
View File
@@ -0,0 +1,5 @@
# These are supported funding model platforms
github: sampotts
patreon: plyr
open_collective: plyr
+1 -1
View File
@@ -1,3 +1,3 @@
PLEASE USE OUR SPECIFIC ISSUE TEMPLATES for bug reports, features and improvement suggestions. PLEASE USE OUR SPECIFIC ISSUE TEMPLATES for bug reports, features and improvement suggestions.
Our issue tracker is not for support questions. If you need help, follow our support instructions: https://github.com/sampotts/plyr/blob/master/contributing.md#support Our issue tracker is not for support questions. If you need help, follow our support instructions: https://github.com/sampotts/plyr/blob/master/CONTRIBUTING.md#support
-2
View File
@@ -3,9 +3,7 @@ node_modules
credentials.json credentials.json
*.mp4 *.mp4
!dist/blank.mp4 !dist/blank.mp4
index-*.html
npm-debug.log npm-debug.log
yarn-error.log yarn-error.log
package-lock.json
*.webm *.webm
.idea/ .idea/
+6
View File
@@ -0,0 +1,6 @@
tasks:
- before: npm install && npm i gulp -g
command: gulp
ports:
- port: 3000
onOpen: open-preview
+1
View File
@@ -0,0 +1 @@
v13.8.0
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"useTabs": false, "useTabs": false,
"tabWidth": 4, "tabWidth": 2,
"singleQuote": true, "singleQuote": true,
"trailingComma": "all", "trailingComma": "all",
"printWidth": 120 "printWidth": 120
+1 -1
View File
@@ -10,7 +10,7 @@
} }
], ],
"string-no-newline": null, "string-no-newline": null,
"indentation": 4, "indentation": 2,
"string-quotes": "single", "string-quotes": "single",
"max-nesting-depth": 2, "max-nesting-depth": 2,
"plugin/selector-bem-pattern": { "plugin/selector-bem-pattern": {
+4 -4
View File
@@ -2,11 +2,11 @@
// See http://go.microsoft.com/fwlink/?LinkId=827846 // See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format // for the documentation about the extensions.json format
"recommendations": [ "recommendations": [
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"wix.vscode-import-cost",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"shinnn.stylelint", "wayou.vscode-todo-highlight",
"wayou.vscode-todo-highlight" "wix.vscode-import-cost",
"stylelint.vscode-stylelint",
"pflannery.vscode-versionlens"
] ]
} }
+1183
View File
File diff suppressed because it is too large Load Diff
+25 -11
View File
@@ -7,15 +7,17 @@ We welcome bug reports, feature requests and pull requests. If you want to help
Before asking questions, read our [documentation](https://github.com/sampotts/plyr) and [FAQ](https://github.com/sampotts/plyr/wiki/FAQ). 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 If these doesn't answer your question
* Use [Stack Overflow](https://stackoverflow.com/) for questions that doesn't directly involve Plyr. This includes for example how to use Javascript, CSS or HTML5 media in general, and how to use other frameworks, libraries and technology.
* Use [our Slack](https://bit.ly/plyr-chat) if you need help using Plyr or have questions about Plyr. - Use [Stack Overflow](https://stackoverflow.com/) for questions that doesn't directly involve Plyr. This includes for example how to use Javascript, CSS or HTML5 media in general, and how to use other frameworks, libraries and technology.
- Use [our Slack](https://bit.ly/plyr-chat) if you need help using Plyr or have questions about Plyr.
## Commenting ## 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. 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. Asking for the status on issues is discouraged. Unless someone has explicitly said in an issue that it's work in progress, most likely that means no one is working on it. We have a lot to do, and it may not be a top priority for us.
We *may* moderate discussions. We do this to avoid threads being "hijacked", to avoid confusion in case the content is misleading or outdated, and to avoid bothering people with github notifications. We _may_ moderate discussions. We do this to avoid threads being "hijacked", to avoid confusion in case the content is misleading or outdated, and to avoid bothering people with github notifications.
## Creating issues ## Creating issues
@@ -23,18 +25,30 @@ Please follow the instructions in our issue templates. Don't use github issues t
## Contributing features and documentation ## Contributing features and documentation
* If you want to add a feature or make critical changes, you may want to ensure that this is something we also want (so you don't waste your time). Ask us about this in the corresponding issue if there is one, or on [our Slack](https://bit.ly/plyr-chat) otherwise. - If you want to add a feature or make critical changes, you may want to ensure that this is something we also want (so you don't waste your time). Ask us about this in the corresponding issue if there is one, or on [our Slack](https://bit.ly/plyr-chat) otherwise.
* Fork Plyr, and create a new branch in your fork, based on the **develop** branch - Fork Plyr, and create a new branch in your fork, based on the **develop** branch
* To test locally, you can use the demo. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build while you are working, and run a local server from the repository root directory. If you have Python installed, this command should work: `python -m SimpleHTTPServer 8080`. Then go to `http://localhost:8080/demo/` - To test locally, you can use the demo site. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build and it will run a local web server for development and watch for any changes.
* Develop and test your modifications. ### Online one-click setup for contributing
* Preferably commit your changes as independent logical chunks, with meaningful messages. Make sure you do not commit unnecessary files or changes, such as the build output, or logging and breakpoints you added for testing. You can use Gitpod (a free online VS Code-like IDE) for contributing. With a single click it will launch a workspace and automatically:
* If your modifications changes the documented behavior or add new features, document these changes in readme.md. - clone the plyr repo.
- install the dependencies.
- run `gulp` to the start the server.
* When finished, push the changes to your GitHub repository and send a pull request to **develop**. Describe what your PR does. So that you can start straight away.
* If the Travis build fails, or if you get a code review with change requests, you can fix these by pushing new or rebased commits to the branch. [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/from-referrer/)
- Develop and test your modifications.
- Preferably commit your changes as independent logical chunks, with meaningful messages. Make sure you do not commit unnecessary files or changes, such as the build output, or logging and breakpoints you added for testing.
- If your modifications changes the documented behavior or add new features, document these changes in [README.md](README.md).
- When finished, push the changes to your GitHub repository and send a pull request 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.
View File
View File
+186 -47
View File
@@ -1,18 +1,19 @@
Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vimeo media player that supports [_modern_](#browser-support) browsers. Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vimeo media player that supports [_modern_](#browser-support) browsers.
[Checkout the demo](https://plyr.io) - [Donate](#donate) - [Slack](https://bit.ly/plyr-chat) - [![npm version](https://badge.fury.io/js/plyr.svg)](https://badge.fury.io/js/plyr) [Checkout the demo](https://plyr.io) - [Donate](#donate) - [Slack](https://bit.ly/plyr--chat)
[![npm version](https://badge.fury.io/js/plyr.svg)](https://badge.fury.io/js/plyr) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/sampotts/plyr) [![Financial Contributors on Open Collective](https://opencollective.com/plyr/all/badge.svg?label=financial+contributors)](https://opencollective.com/plyr)
[![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png?v=3)](https://plyr.io) [![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png?v=3)](https://plyr.io)
# Features # Features
- 📼 **HTML Video & Audio, YouTube & Vimeo** - support for the major formats
- 💪 **Accessible** - full support for VTT captions and screen readers - 💪 **Accessible** - full support for VTT captions and screen readers
- 🔧 **[Customisable](#html)** - make the player look how you want with the markup you want - 🔧 **[Customizable](#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 - 😎 **Clean HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
`<span>` or `<a href="#">` button hacks `<span>` or `<a href="#">` button hacks
- 📱 **Responsive** - works with any screen size - 📱 **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 - 💵 **[Monetization](#ads)** - make money from your videos
- 📹 **[Streaming](#demos)** - support for hls.js, Shaka and dash.js streaming playback - 📹 **[Streaming](#demos)** - support for hls.js, Shaka and dash.js streaming playback
- 🎛 **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API - 🎛 **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
@@ -25,7 +26,7 @@ Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vi
- 📖 **Multiple captions** - support for multiple caption tracks - 📖 **Multiple captions** - support for multiple caption tracks
- 🌎 **i18n support** - support for internationalization of controls - 🌎 **i18n support** - support for internationalization of controls
- 👌 **[Preview thumbnails](#preview-thumbnails)** - support for displaying preview thumbnails - 👌 **[Preview thumbnails](#preview-thumbnails)** - support for displaying preview thumbnails
- 🤟 **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required - 🤟 **No frameworks** - written in "vanilla" ES6 JavaScript, no jQuery required
- 💁‍♀️ **SASS** - to include in your build processes - 💁‍♀️ **SASS** - to include in your build processes
### Demos ### Demos
@@ -41,7 +42,7 @@ Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.o
### HTML5 Video ### HTML5 Video
```html ```html
<video poster="/path/to/poster.jpg" id="player" playsinline controls> <video id="player" playsinline controls data-poster="/path/to/poster.jpg">
<source src="/path/to/video.mp4" type="video/mp4" /> <source src="/path/to/video.mp4" type="video/mp4" />
<source src="/path/to/video.webm" type="video/webm" /> <source src="/path/to/video.webm" type="video/webm" />
@@ -50,6 +51,8 @@ Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.o
</video> </video>
``` ```
**Note**: The poster image should be specified using `data-poster`. This is to prevent it [being downloaded twice](https://github.com/sampotts/plyr/issues/1531). If you're sure the image will be cached, you can still use the `poster` attribute for true progressive enhancement.
### HTML5 Audio ### HTML5 Audio
```html ```html
@@ -109,7 +112,15 @@ Or the `<div>` non progressively enhanced method:
## JavaScript ## JavaScript
Include the `plyr.js` script before the closing `</body>` tag and then in your JS create a new instance of Plyr as below. You can use Plyr as an ES6 module as follows:
```javascript
import Plyr from 'plyr';
const player = new Plyr('#player');
```
Alternatively you can include the `plyr.js` script before the closing `</body>` tag and then in your JS create a new instance of Plyr as below.
```html ```html
<script src="path/to/plyr.js"></script> <script src="path/to/plyr.js"></script>
@@ -123,18 +134,18 @@ See [initialising](#initialising) for more information on advanced setups.
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build. You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
```html ```html
<script src="https://cdn.plyr.io/3.5.2/plyr.js"></script> <script src="https://cdn.plyr.io/3.6.2/plyr.js"></script>
``` ```
...or... ...or...
```html ```html
<script src="https://cdn.plyr.io/3.5.2/plyr.polyfilled.js"></script> <script src="https://cdn.plyr.io/3.6.2/plyr.polyfilled.js"></script>
``` ```
## CSS ## CSS
Include the `plyr.css` stylsheet into your `<head>` Include the `plyr.css` stylsheet into your `<head>`.
```html ```html
<link rel="stylesheet" href="path/to/plyr.css" /> <link rel="stylesheet" href="path/to/plyr.css" />
@@ -143,13 +154,13 @@ Include the `plyr.css` stylsheet into your `<head>`
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following: If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
```html ```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.5.2/plyr.css" /> <link rel="stylesheet" href="https://cdn.plyr.io/3.6.2/plyr.css" />
``` ```
## SVG Sprite ## SVG Sprite
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.5.2/plyr.svg`. reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.6.2/plyr.svg`.
# Ads # Ads
@@ -161,12 +172,109 @@ Plyr has partnered up with [vi.ai](https://vi.ai/publisher-video-monetization/?a
Any questions regarding the ads can be sent straight to vi.ai and any issues with rendering raised through GitHub issues. Any questions regarding the ads can be sent straight to vi.ai and any issues with rendering raised through GitHub issues.
If you do not wish to use Vi, you can set your own `ads.tagUrl` [option](#options).
# Advanced # Advanced
## SASS ## Customizing the CSS
You can use `bundle.scss` file included in `/src` as part of your build and change variables to suit your design. The SASS require you to If you want to change any design tokens used for the rendering of the player, you can do so using [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties).
use the [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) plugin (you should be already!) as all declarations use the W3C definitions.
Here's a list of the properties and what they are used for:
| Name | Description | Default / Fallback |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `--plyr-color-main` | The primary UI color. | ![#f03c15](https://placehold.it/15/00b3ff/000000?text=+) `#00b3ff` |
| `--plyr-tab-focus-color` | The color used for the dotted outline when an element is `:focus-visible` (equivalent) keyboard focus. | `--plyr-color-main` |
| `--plyr-badge-background` | The background color for badges in the menu. | ![#4a5464](https://placehold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-badge-text-color` | The text color for badges. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-badge-border-radius` | The border radius used for badges. | `2px` |
| `--plyr-tab-focus-color` | The color used to highlight tab (keyboard) focus. | `--plyr-color-main` |
| `--plyr-captions-background` | The color for the background of captions. | `rgba(0, 0, 0, 0.8)` |
| `--plyr-captions-text-color` | The color used for the captions text. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-control-icon-size` | The size of the icons used in the controls. | `18px` |
| `--plyr-control-spacing` | The space between controls (sometimes used in a multiple - e.g. `10px / 2 = 5px`). | `10px` |
| `--plyr-control-padding` | The padding inside controls. | `--plyr-control-spacing * 0.7` (`7px`) |
| `--plyr-control-radius` | The border radius used on controls. | `3px` |
| `--plyr-control-toggle-checked-background` | The background color used for checked menu items. | `--plyr-color-main` |
| `--plyr-video-controls-background` | The background for the video controls. | `linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75))` |
| `--plyr-video-control-color` | The text/icon color for video controls. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-video-control-color-hover` | The text/icon color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-video-control-background-hover` | The background color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | `--plyr-color-main` |
| `--plyr-audio-controls-background` | The background for the audio controls. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-audio-control-color` | The text/icon color for audio controls. | ![#4a5464](https://placehold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-audio-control-color-hover` | The text/icon color used when audio controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-audio-control-background-hover` | The background color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | `--plyr-color-main` |
| `--plyr-menu-background` | The background color for menus. | `rgba(255, 255, 255, 0.9)` |
| `--plyr-menu-color` | The text/icon color for menu items. | ![#4a5464](https://placehold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-menu-shadow` | The shadow used on menus. | `0 1px 2px rgba(0, 0, 0, 0.15)` |
| `--plyr-menu-radius` | The border radius on the menu. | `4px` |
| `--plyr-menu-arrow-size` | The size of the arrow on the bottom of the menu. | `6px` |
| `--plyr-menu-item-arrow-color` | The color of the arrows in the menu. | ![#728197](https://placehold.it/15/728197/000000?text=+) `#728197` |
| `--plyr-menu-item-arrow-size` | The size of the arrows in the menu. | `4px` |
| `--plyr-menu-border-color` | The border color for the bottom of the back button in the top of the sub menu pages. | ![#dcdfe5](https://placehold.it/15/dcdfe5/000000?text=+) `#dcdfe5` |
| `--plyr-menu-border-shadow-color` | The shadow below the border of the back button in the top of the sub menu pages. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-progress-loading-size` | The size of the stripes in the loading state in the scrubber. | `25px` |
| `--plyr-progress-loading-background` | The background color on the loading state in the scrubber. | `rgba(35, 40, 47, 0.6)` |
| `--plyr-video-progress-buffered-background` | The fill color for the buffer indication in the scrubber for video. | `rgba(255, 255, 255, 0.25)` |
| `--plyr-audio-progress-buffered-background` | The fill color for the buffer indication in the scrubber for audio. | `rgba(193, 200, 209, 0.6)` |
| `--plyr-range-thumb-height` | The height of the scrubber handle/thumb. | `13px` |
| `--plyr-range-thumb-background` | The background of the scrubber handle/thumb. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-range-thumb-shadow` | The shadow of the scrubber handle/thumb. | `0 1px 1px rgba(215, 26, 18, 0.15), 0 0 0 1px rgba(215, 26, 18, 0.2)` |
| `--plyr-range-thumb-active-shadow-width` | The width of the shadow when the scrubber handle/thumb is `:active` (pressed). | `3px` |
| `--plyr-range-track-height` | The height of the scrubber/progress track. | `5px` |
| `--plyr-range-fill-background` | The fill color of the scrubber/progress. | `--plyr-color-main` |
| `--plyr-video-range-track-background` | The background of the scrubber/progress. | `--plyr-video-progress-buffered-background` |
| `--plyr-video-range-thumb-active-shadow-color` | The color of the shadow when the video scrubber handle/thumb is `:active` (pressed). | `rgba(255, 255, 255, 0.5)` |
| `--plyr-audio-range-track-background` | The background of the scrubber/progress. | `--plyr-video-progress-buffered-background` |
| `--plyr-audio-range-thumb-active-shadow-color` | The color of the shadow when the audio scrubber handle/thumb is `:active` (pressed). | `rgba(215, 26, 18, 0.1)` |
| `--plyr-tooltip-background` | The background color for tooltips. | `rgba(255, 255, 255, 0.9)` |
| `--plyr-tooltip-color` | The text color for tooltips. | ![#4a5464](https://placehold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-tooltip-padding` | The padding for tooltips. | `calc(var(--plyr-control-spacing) / 2))` |
| `--plyr-tooltip-arrow-size` | The size of the arrow under tooltips. | `4px` |
| `--plyr-tooltip-radius` | The border radius on tooltips. | `3px` |
| `--plyr-tooltip-shadow` | The shadow on tooltips. | `0 1px 2px rgba(0, 0, 0, 0.15)` |
| `--plyr-font-family` | The font family used in the player. | |
| `--plyr-font-size-base` | The base font size. Mainly used for captions. | `15px` |
| `--plyr-font-size-small` | The smaller font size. Mainly used for captions. | `13px` |
| `--plyr-font-size-large` | The larger font size. Mainly used for captions. | `18px` |
| `--plyr-font-size-xlarge` | The even larger font size. Mainly used for captions. | `21px` |
| `--plyr-font-size-time` | The font size for the time. | `--plyr-font-size-small` |
| `--plyr-font-size-menu` | The font size used in the menu. | `--plyr-font-size-small` |
| `--plyr-font-size-badge` | The font size used for badges. | `9px` |
| `--plyr-font-weight-regular` | The regular font weight. | `400` |
| `--plyr-font-weight-bold` | The bold font weight. | `600` |
| `--plyr-line-height` | The line height used within the player. | `1.7` |
| `--plyr-font-smoothing` | Whether to enable font antialiasing within the player. | `false` |
You can set them in your CSS for all players:
```css
:root {
--plyr-color-main: #1ac266;
}
```
...or for a specific class name:
```css
.player {
--plyr-color-main: #1ac266;
}
```
...or in your HTML:
```html
<video class="player" style="--plyr-color-main: #1ac266;">
...
</vieo>
```
### SASS
You can use `plyr.scss` file included in `/src/sass` as part of your build and change variables to suit your design. The SASS requires you to
use [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) (you should be already!) as all declarations use the W3C definitions.
The HTML markup uses the BEM methodology with `plyr` as the block, e.g. `.plyr__controls`. You can change the class hooks in the options to match any custom CSS 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. you write. Check out the JavaScript source for more on this.
@@ -204,7 +312,7 @@ WebVTT captions are supported. To add a caption track, check the HTML example ab
You can specify a range of arguments for the constructor to use: You can specify a range of arguments for the constructor to use:
- A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) - A [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)
- A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) - A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
- A [jQuery](https://jquery.com) object - A [jQuery](https://jquery.com) object
@@ -212,7 +320,7 @@ _Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element
#### Single player #### Single player
Passing a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList): Passing a CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector):
```javascript ```javascript
const player = new Plyr('#player'); const player = new Plyr('#player');
@@ -238,7 +346,7 @@ You have two choices here. You can either use a simple array loop to map the con
const players = Array.from(document.querySelectorAll('.js-player')).map(p => new Plyr(p)); const players = Array.from(document.querySelectorAll('.js-player')).map(p => new Plyr(p));
``` ```
...or use a static method where you can pass a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of elements, or a [JQuery](https://jquery.com) object: ...or use a static method where you can pass a [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement), or a [JQuery](https://jquery.com) object:
```javascript ```javascript
const players = Plyr.setup('.js-player'); const players = Plyr.setup('.js-player');
@@ -268,14 +376,14 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | Boolean | `true` | Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below. | | `enabled` | Boolean | `true` | Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below. |
| `debug` | Boolean | `false` | Display debugging information in the console | | `debug` | Boolean | `false` | Display debugging information in the console |
| `controls` | Array, Function or Element | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function; `id` (the unique id for the player), `seektime` (the seektime step in seconds), and `title` (the media title). See [controls.md](controls.md) for more info on how the html needs to be structured. | | `controls` | Array, Function or Element | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function; `id` (the unique id for the player), `seektime` (the seektime step in seconds), and `title` (the media title). See [CONTROLS.md](CONTROLS.md) for more info on how the html needs to be structured. |
| `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If you're using the default controls are used then you can specify which settings to show in the menu | | `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If the default controls are used, you can specify which settings to show in the menu |
| `i18n` | Object | See [defaults.js](/src/js/config/defaults.js) | Used for internationalization (i18n) of the text within the UI. | | `i18n` | Object | See [defaults.js](/src/js/config/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
| `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. | | `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. |
| `iconUrl` | String | `null` | Specify a URL or path to the SVG sprite. See the [SVG section](#svg) for more info. | | `iconUrl` | String | `null` | Specify a URL or path to the SVG sprite. See the [SVG section](#svg) for more info. |
| `iconPrefix` | String | `plyr` | Specify the id prefix for the icons used in the default controls (e.g. "plyr-play" would be "plyr"). This is to prevent clashes if you're using your own SVG sprite but with the default controls. Most people can ignore this option. | | `iconPrefix` | String | `plyr` | Specify the id prefix for the icons used in the default controls (e.g. "plyr-play" would be "plyr"). This is to prevent clashes if you're using your own SVG sprite but with the default controls. Most people can ignore this option. |
| `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. | | `blankVideo` | String | `https://cdn.plyr.io/static/blank.mp4` | Specify a URL or path to a blank video file used to properly cancel network requests. |
| `autoplay` | Boolean | `false` | Autoplay the media on load. This is generally advised against on UX grounds. It is also disabled by default in some browsers. If the `autoplay` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. | | `autoplay`&sup2; | Boolean | `false` | Autoplay the media on load. If the `autoplay` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
| `autopause`&sup1; | Boolean | `true` | Only allow one player playing at once. | | `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. | | `seekTime` | Number | `10` | The time, in seconds, to seek when a user hits fast forward or rewind. |
| `volume` | Number | `1` | A number, between 0 and 1, representing the initial volume of the player. | | `volume` | Number | `1` | A number, between 0 and 1, representing the initial volume of the player. |
@@ -292,19 +400,24 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. | | `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. | | `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 (`true`/`false`/`'force'`). `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) | | `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false, container: null }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution (`true`/`false`/`'force'`). `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls). `container`: A selector for an ancestor of the player element, allows contextual content to remain visual in fullscreen mode. Non-ancestors are ignored. |
| `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. | | `ratio` | String | `null` | Force an aspect ratio for all videos. The format is `'w:h'` - e.g. `'16:9'` or `'4:3'`. If this is not specified then the default for HTML5 and Vimeo is to use the native resolution of the video. As dimensions are not available from YouTube via SDK, 16:9 is forced as a sensible default. |
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. | | `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: Options to display in the menu. Most browsers will refuse to play slower than 0.5. | | `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: The speed options to display in the UI. YouTube and Vimeo will ignore any options outside of the 0.5-2 range, so options outside of this range will be hidden automatically. |
| `quality` | Object | `{ default: '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. | | `quality` | Object | `{ default: 576, options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240] }` | `default` is the default quality level (if it exists in your sources). `options` are the options to display. This is used to filter the available sources. |
| `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. | | `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. |
| `ads` | Object | `{ enabled: false, publisherId: '' }` | `enabled`: Whether to enable advertisements. `publisherId`: Your unique [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) publisher ID. | | `ads` | Object | `{ enabled: false, publisherId: '', tagUrl: '' }` | `enabled`: Whether to enable advertisements. `publisherId`: Your unique [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) publisher ID. `tagUrl` is a URL for a custom VAST tag if you're not using Vi. |
| `urls` | Object | See source. | If you wish to override any API URLs then you can do so here. You can also set a custom download URL for the download button. | | `urls` | Object | See source. | If you wish to override any API URLs then you can do so here. You can also set a custom download URL for the download button. |
| `vimeo` | Object | `{ byline: false, portrait: false, title: false, speed: true, transparent: false }` | See [Vimeo embed options](https://github.com/vimeo/player.js/#embed-options). Some are set automatically based on other config options, namely: `loop`, `autoplay`, `muted`, `gesture`, `playsinline` | | `vimeo` | Object | `{ byline: false, portrait: false, title: false, speed: true, transparent: false }` | See [Vimeo embed options](https://github.com/vimeo/player.js/#embed-options). Some are set automatically based on other config options, namely: `loop`, `autoplay`, `muted`, `gesture`, `playsinline` |
| `youtube` | Object | `{ noCookie: false, rel: 0, showinfo: 0, iv_load_policy: 3, modestbranding: 1 }` | See [YouTube embed options](https://developers.google.com/youtube/player_parameters#Parameters). The only custom option is `noCookie` to use an alternative to YouTube that doesn't use cookies (useful for GDPR, etc). Some are set automatically based on other config options, namely: `autoplay`, `hl`, `controls`, `disablekb`, `playsinline`, `cc_load_policy`, `cc_lang_pref`, `widget_referrer` | | `youtube` | Object | `{ noCookie: false, rel: 0, showinfo: 0, iv_load_policy: 3, modestbranding: 1 }` | See [YouTube embed options](https://developers.google.com/youtube/player_parameters#Parameters). The only custom option is `noCookie` to use an alternative to YouTube that doesn't use cookies (useful for GDPR, etc). Some are set automatically based on other config options, namely: `autoplay`, `hl`, `controls`, `disablekb`, `playsinline`, `cc_load_policy`, `cc_lang_pref`, `widget_referrer` |
| `previewThumbnails` | Object | `{ enabled: false, src: '' }` | `enabled`: Whether to enable the preview thumbnails (they must be generated by you). `src` must be either a string or an array of strings representing URLs for the VTT files containing the image URL(s). Learn more about [preview thumbnails](#preview-thumbnails) below. | | `previewThumbnails` | Object | `{ enabled: false, src: '' }` | `enabled`: Whether to enable the preview thumbnails (they must be generated by you). `src` must be either a string or an array of strings representing URLs for the VTT files containing the image URL(s). Learn more about [preview thumbnails](#preview-thumbnails) below. |
1. Vimeo only 1. Vimeo only
2. Autoplay is generally not recommended as it is seen as a negative user experience. It is also disabled in many browsers. Before raising issues, do your homework. More info can be found here:
- https://webkit.org/blog/6784/new-video-policies-for-ios/
- https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
- https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/
# API # API
@@ -338,10 +451,10 @@ player.fullscreen.enter(); // Enter fullscreen
``` ```
| Method | Parameters | Description | | Method | Parameters | Description |
| ------------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------- | | -------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `play()`&sup1; | - | Start playback. | | `play()`&sup1; | - | Start playback. |
| `pause()` | - | Pause playback. | | `pause()` | - | Pause playback. |
| `togglePlay(toggle)` | Boolean | Toggle playback, if no parameters are passed, it will toggle based on current status. | | `togglePlay(toggle)`&sup1; | Boolean | Toggle playback, if no parameters are passed, it will toggle based on current status. |
| `stop()` | - | Stop playback and reset to start. | | `stop()` | - | Stop playback and reset to start. |
| `restart()` | - | Restart playback. | | `restart()` | - | Restart playback. |
| `rewind(seekTime)` | Number | Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used. | | `rewind(seekTime)` | Number | Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used. |
@@ -360,7 +473,7 @@ player.fullscreen.enter(); // Enter fullscreen
| `supports(type)` | String | Check support for a mime type. | | `supports(type)` | String | Check support for a mime type. |
| `destroy()` | - | Destroy the instance and garbage collect any elements. | | `destroy()` | - | Destroy the instance and garbage collect any elements. |
1. For HTML5 players, `play()` will return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) in _some_ browsers - WebKit and Mozilla [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) at time of writing. 1. For HTML5 players, `play()` will return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) for most browsers - e.g. Chrome, Firefox, Opera, Safari and Edge [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) at time of writing.
## Getters and Setters ## Getters and Setters
@@ -404,10 +517,11 @@ player.fullscreen.active; // false;
| `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. | | `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.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. | | `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+. | | `pip`&sup1; | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ (on MacOS Sierra+ and iOS 10+) and Chrome 70+. |
| `ratio` | ✓ | ✓ | Gets or sets the video aspect ratio. The setter accepts a string in the same format as the `ratio` option. |
| `download` | ✓ | ✓ | Gets or sets the URL for the download button. The setter accepts a string containing a valid absolute URL. |
1. YouTube only. HTML5 will follow. 1. HTML5 only
2. HTML5 only
### The `.source` setter ### The `.source` setter
@@ -432,6 +546,9 @@ player.source = {
}, },
], ],
poster: '/path/to/poster.jpg', poster: '/path/to/poster.jpg',
previewThumbnails: {
src: '/path/to/thumbnails.vtt',
},
tracks: [ tracks: [
{ {
kind: 'captions', kind: 'captions',
@@ -483,8 +600,6 @@ player.source = {
}; };
``` ```
_Note_: `src` can be the video ID or URL
Vimeo example Vimeo example
```javascript ```javascript
@@ -502,12 +617,13 @@ player.source = {
_Note:_ `src` property for YouTube and Vimeo can either be the video ID or the whole URL. _Note:_ `src` property for YouTube and Vimeo can either be the video ID or the whole URL.
| Property | Type | Description | | Property | Type | Description |
| -------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | String | Either `video` or `audio`. _Note:_ YouTube and Vimeo are currently not supported as audio sources. | | `type` | String | Either `video` or `audio`. _Note:_ YouTube and Vimeo are currently not supported as audio sources. |
| `title` | String | _Optional._ Title of the new media. Used for the `aria-label` attribute on the play button, and outer container. YouTube and Vimeo are populated automatically. | | `title` | String | _Optional._ Title of the new media. Used for the `aria-label` attribute on the play button, and outer container. YouTube and Vimeo are populated automatically. |
| `sources` | Array | This is an array of sources. For HTML5 media, the properties of this object are mapped directly to HTML attributes so more can be added to the object if required. | | `sources` | Array | This is an array of sources. For HTML5 media, the properties of this object are mapped directly to HTML attributes so more can be added to the object if required. |
| `poster`&sup1; | String | The URL for the poster image (HTML5 video only). | | `poster`&sup1; | String | The URL for the poster image (HTML5 video only). |
| `tracks`&sup1; | String | An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above, it will render as `<track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default>` and similar for the French version. Booleans are converted to HTML5 value-less attributes. | | `tracks`&sup1; | String | An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above, it will render as `<track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default>` and similar for the French version. Booleans are converted to HTML5 value-less attributes. |
| `previewThumbnails`&sup1; | Object | The same object like in the `previewThumbnails` constructor option. This means you can either change the thumbnails vtt via the `src` key or disable the thumbnails plugin for the next video by passing `{ enabled: false }`. |
1. HTML5 only 1. HTML5 only
@@ -617,7 +733,7 @@ Fullscreen in Plyr is supported by all browsers that [currently support it](http
Plyr supports the last 2 versions of most _modern_ browsers. Plyr supports the last 2 versions of most _modern_ browsers.
| Browser | Supported | | Browser | Supported |
| ------------- | ------------- | | ------------- | --------------- |
| Safari | ✓ | | Safari | ✓ |
| Mobile Safari | ✓&sup1; | | Mobile Safari | ✓&sup1; |
| Firefox | ✓ | | Firefox | ✓ |
@@ -625,7 +741,7 @@ Plyr supports the last 2 versions of most _modern_ browsers.
| Opera | ✓ | | Opera | ✓ |
| Edge | ✓ | | Edge | ✓ |
| IE11 | ✓&sup3; | | IE11 | ✓&sup3; |
| IE10 | ✓&sup2;&sup3; | | IE10 | ✓<sup>2,3</sup> |
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide. 1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide.
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)). 2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)).
@@ -649,13 +765,13 @@ The arguments are:
- Provider (`html5`, `youtube` or `vimeo`) - Provider (`html5`, `youtube` or `vimeo`)
- Whether the player has the `playsinline` attribute (only applicable to iOS 10+) - Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
## Disable support programatically ## Disable support programmatically
The `enabled` option can be used to disable certain User Agents. For example, if you don't want to use Plyr for smartphones, you could use: The `enabled` option can be used to disable certain User Agents. For example, if you don't want to use Plyr for smartphones, you could use:
```javascript ```javascript
{ {
enabled: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent); enabled: !/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
} }
``` ```
@@ -666,13 +782,14 @@ If a User Agent is disabled but supports `<video>` and `<audio>` natively, it wi
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks: Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
| Type | Maintainer | Link | | Type | Maintainer | Link |
| --------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | --------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) | | WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
| Angular | Simon Bobrov ([@smnbbrv](https://github.com/smnbbrv)) | [https://github.com/smnbbrv/ngx-plyr](https://github.com/smnbbrv/ngx-plyr) | | Angular | Simon Bobrov ([@smnbbrv](https://github.com/smnbbrv)) | [https://github.com/smnbbrv/ngx-plyr](https://github.com/smnbbrv/ngx-plyr) |
| React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) | | React | Chintan Prajapati ([@chintan9](https://github.com/chintan9)) | [https://github.com/chintan9/plyr-react](https://github.com/chintan9/plyr-react) |
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) | | Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) | | Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
| Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) | | Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
| REDAXO | FriendsOfRedaxo / skerbis ([@skerbis](https://friendsofredaxo.github.io)) | [https://github.com/FriendsOfREDAXO/plyr](https://github.com/FriendsOfREDAXO/plyr) |
# Issues # Issues
@@ -697,6 +814,7 @@ Plyr costs money to run, not only my time. I donate my time for free as I enjoy
- [HTML5 Weekly #177](http://html5weekly.com/issues/177) - [HTML5 Weekly #177](http://html5weekly.com/issues/177)
- [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f) - [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/) - [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/)
- [Front End Focus #177](https://frontendfoc.us/issues/177)
- [Hacker News](https://news.ycombinator.com/item?id=9136774) - [Hacker News](https://news.ycombinator.com/item?id=9136774)
- [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04) - [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) - [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player)
@@ -716,13 +834,11 @@ Plyr costs money to run, not only my time. I donate my time for free as I enjoy
- [Sparkk TV](https://www.sparkktv.com/) - [Sparkk TV](https://www.sparkktv.com/)
- [@halfhalftravel](https://www.halfhalftravel.com/) - [@halfhalftravel](https://www.halfhalftravel.com/)
Let me know on [Twitter](https://twitter.com/sam_potts) I can add you to the above list. It'd be awesome to see how you're using Plyr :-) If you want to be added to the list, open a pull request. It'd be awesome to see how you're using Plyr 😎
# Useful links and credits # Useful links and credits
Credit to the PayPal HTML5 Video player from which Plyr's caption functionality was originally ported from: - [PayPal's Accessible HTML5 Video Player (which Plyr was originally ported from)](https://github.com/paypal/accessible-html5-video-player)
- [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) - [An awesome guide for Plyr in Japanese!](http://syncer.jp/how-to-use-plyr-io) by [@arayutw](https://twitter.com/arayutw)
# Thanks # Thanks
@@ -735,6 +851,29 @@ Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN servic
Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site. Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site.
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
<a href="https://github.com/sampotts/plyr/graphs/contributors"><img src="https://opencollective.com/plyr/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/plyr/contribute)]
#### Individuals
<a href="https://opencollective.com/plyr"><img src="https://opencollective.com/plyr/individuals.svg?width=890"></a>
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/plyr/contribute)]
<a href="https://opencollective.com/plyr/organization/0/website"><img src="https://opencollective.com/plyr/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/plyr/organization/1/website"><img src="https://opencollective.com/plyr/organization/1/avatar.svg"></a><a href="https://opencollective.com/plyr/organization/2/website"><img src="https://opencollective.com/plyr/organization/2/avatar.svg"></a>
# Copyright and License # Copyright and License
[The MIT license](license.md) [The MIT license](LICENSE.md)
+4 -2
View File
@@ -10,13 +10,15 @@
"src": "./src/js/plyr.polyfilled.js", "src": "./src/js/plyr.polyfilled.js",
"dist": "./dist/", "dist": "./dist/",
"formats": ["es", "umd"], "formats": ["es", "umd"],
"namespace": "Plyr" "namespace": "Plyr",
"polyfill": true
}, },
"demo.js": { "demo.js": {
"src": "./demo/src/js/demo.js", "src": "./demo/src/js/demo.js",
"dist": "./demo/dist/", "dist": "./demo/dist/",
"formats": ["iife"], "formats": ["iife"],
"namespace": "Demo" "namespace": "Demo",
"polyfill": true
} }
}, },
"css": { "css": {
-1072
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
+21702 -5071
View File
File diff suppressed because it is too large Load Diff
+18 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

+1 -1
View File
File diff suppressed because one or more lines are too long
+23 -33
View File
@@ -17,11 +17,8 @@
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16" /> <link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16" />
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png" /> <link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png" />
<!-- Opengraph --> <!-- Open Graph -->
<meta <meta property="og:title" content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player" />
property="og:title"
content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player"
/>
<meta property="og:site_name" content="Plyr" /> <meta property="og:site_name" content="Plyr" />
<meta property="og:url" content="https://plyr.io" /> <meta property="og:url" content="https://plyr.io" />
<meta property="og:image" content="https://cdn.plyr.io/static/icons/1200x630.png" /> <meta property="og:image" content="https://cdn.plyr.io/static/icons/1200x630.png" />
@@ -50,12 +47,23 @@
type="font/woff2" type="font/woff2"
href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2" href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2"
/> />
<!-- Google Analytics-->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-132699580-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-132699580-1');
</script>
</head> </head>
<body> <body>
<div class="grid"> <div class="grid">
<header> <header>
<h1>Plyr</h1> <h1>Pl<span>a</span>y<span>e</span>r</h1>
<p> <p>
A simple, accessible and customisable media player for A simple, accessible and customisable media player for
<button type="button" class="faux-link" data-source="video"> <button type="button" class="faux-link" data-source="video">
@@ -98,7 +106,7 @@
</p> </p>
<p> <p>
Premium video monitization from Premium video monetization from
<a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border"> <a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border">
<img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi" /> <img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi" />
<span class="sr-only">ai.vi</span> <span class="sr-only">ai.vi</span>
@@ -106,8 +114,7 @@
</p> </p>
<div class="call-to-action"> <div class="call-to-action">
<span class="button--with-count"> <a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr">
<a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr-button">
<svg class="icon" role="presentation"> <svg class="icon" role="presentation">
<title>GitHub</title> <title>GitHub</title>
<path <path
@@ -121,17 +128,15 @@
</svg> </svg>
Download on GitHub Download on GitHub
</a> </a>
</span>
</div> </div>
</header> </header>
<main> <main>
<div id="container"> <div id="container">
<video <video
controls controls
crossorigin crossorigin
playsinline playsinline
poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" data-poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg"
id="player" id="player"
> >
<!-- Video files --> <!-- Video files -->
@@ -167,9 +172,7 @@
/> />
<!-- Fallback for browsers that don't support the <video> element --> <!-- 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 <a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
>Download</a
>
</video> </video>
</div> </div>
@@ -182,9 +185,7 @@
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z" d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path> ></path>
</svg> </svg>
<a <a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank"
href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323"
target="_blank"
>View From A Blue Moon</a >View From A Blue Moon</a
> >
&copy; Brainfarm &copy; Brainfarm
@@ -206,9 +207,7 @@
</li> </li>
<li class="plyr__cite plyr__cite--youtube" hidden> <li class="plyr__cite plyr__cite--youtube" hidden>
<small> <small>
<a href="https://www.youtube.com/watch?v=bTqVqk7FSmY" target="_blank" <a href="https://www.youtube.com/watch?v=bTqVqk7FSmY" target="_blank">View From A Blue Moon</a>
>View From A Blue Moon</a
>
on&nbsp; on&nbsp;
<span class="color--youtube"> <span class="color--youtube">
<svg class="icon" role="presentation"> <svg class="icon" role="presentation">
@@ -224,7 +223,8 @@
</li> </li>
<li class="plyr__cite plyr__cite--vimeo" hidden> <li class="plyr__cite plyr__cite--vimeo" hidden>
<small> <small>
<a href="https://vimeo.com/76979871" target="_blank">The New Vimeo Player</a> on&nbsp; <a href="https://vimeo.com/40648169" target="_blank">Toob “Wavaphon” Music Video</a>
on&nbsp;
<span class="color--vimeo"> <span class="color--vimeo">
<svg class="icon" role="presentation"> <svg class="icon" role="presentation">
<title>Vimeo</title> <title>Vimeo</title>
@@ -256,23 +256,13 @@
<a <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" 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" target="_blank"
class="js-shr-button" class="js-shr"
>tweet it</a >tweet it</a
> >
👍 👍
</p> </p>
</aside> </aside>
<!-- Polyfills -->
<script
src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL,Math.trunc"
crossorigin="anonymous"
></script>
<!-- Sharing libary (https://shr.one) -->
<script src="https://cdn.shr.one/2.0.0-beta.2/shr.js" crossorigin="anonymous"></script>
<!-- Docs script -->
<script src="dist/demo.js" crossorigin="anonymous"></script> <script src="dist/demo.js" crossorigin="anonymous"></script>
</body> </body>
</html> </html>
+14
View File
@@ -0,0 +1,14 @@
{
"name": "plyr-demo",
"version": "1.0.0",
"description": "Demo for Plyr",
"homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>",
"dependencies": {
"@sentry/browser": "^5.15.5",
"core-js": "^3.6.5",
"custom-event-polyfill": "^1.0.7",
"shr-buttons": "2.0.3",
"url-polyfill": "^1.1.8"
}
}
+67 -205
View File
@@ -1,61 +1,42 @@
// ========================================================================== // ==========================================================================
// Plyr.io demo // Plyr.io demo
// This code is purely for the https://plyr.io website // This code is purely for the https://plyr.io website
// Please see readme.md in the root or github.com/sampotts/plyr // Please see README.md in the root or github.com/sampotts/plyr
// ========================================================================== // ==========================================================================
import Raven from 'raven-js'; import './tab-focus';
import 'custom-event-polyfill';
import 'url-polyfill';
import * as Sentry from '@sentry/browser';
import Shr from 'shr-buttons';
import Plyr from '../../../src/js/plyr'; import Plyr from '../../../src/js/plyr';
import sources from './sources';
import toggleClass from './toggle-class';
(() => { (() => {
const { host } = window.location; const production = 'plyr.io';
const env = {
prod: host === 'plyr.io', // Sentry for demo site (https://plyr.io) only
dev: host === 'dev.plyr.io', if (window.location.host === production) {
}; Sentry.init({
dsn: 'https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555',
whitelistUrls: [production].map(d => new RegExp(`https://(([a-z0-9])+(.))*${d}`)),
});
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
Raven.context(() => {
const selector = '#player'; const selector = '#player';
const container = document.getElementById('container');
if (window.Shr) { // Setup share buttons
window.Shr.setup('.js-shr-button', { Shr.setup('.js-shr', {
count: { count: {
classname: 'button__count', className: 'button__count',
},
wrapper: {
className: 'button--with-count',
}, },
});
}
// Setup tab focus
const tabClassName = 'tab-focus';
// Remove class on blur
document.addEventListener('focusout', event => {
if (!event.target.classList || container.contains(event.target)) {
return;
}
event.target.classList.remove(tabClassName);
});
// Add classname to tabbed elements
document.addEventListener('keydown', event => {
if (event.keyCode !== 9) {
return;
}
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
const focused = document.activeElement;
if (!focused || !focused.classList || container.contains(focused)) {
return;
}
focused.classList.add(tabClassName);
}, 10);
}); });
// Setup the player // Setup the player
@@ -72,19 +53,17 @@ import Plyr from '../../../src/js/plyr';
captions: { captions: {
active: true, active: true,
}, },
keys: {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: { ads: {
enabled: env.prod || env.dev, enabled: window.location.host.includes(production),
publisherId: '918848828995742', publisherId: '918848828995742',
}, },
previewThumbnails: { previewThumbnails: {
enabled: true, enabled: true,
src: [ src: ['https://cdn.plyr.io/static/demo/thumbs/100p.vtt', 'https://cdn.plyr.io/static/demo/thumbs/240p.vtt'],
'https://cdn.plyr.io/static/demo/thumbs/100p.vtt', },
'https://cdn.plyr.io/static/demo/thumbs/240p.vtt', vimeo: {
], // Prevent Vimeo blocking plyr.io demo site
referrerPolicy: 'no-referrer',
}, },
}); });
@@ -93,131 +72,12 @@ import Plyr from '../../../src/js/plyr';
// Setup type toggle // Setup type toggle
const buttons = document.querySelectorAll('[data-source]'); const buttons = document.querySelectorAll('[data-source]');
const types = { const types = Object.keys(sources);
video: 'video', const historySupport = Boolean(window.history && window.history.pushState);
audio: 'audio', let currentType = window.location.hash.substring(1);
youtube: 'youtube', const hasInitialType = currentType.length;
vimeo: 'vimeo',
};
let currentType = window.location.hash.replace('#', '');
const historySupport = window.history && window.history.pushState;
// Toggle class on an element
function toggleClass(element, className, state) {
if (element) {
element.classList[state ? 'add' : 'remove'](className);
}
}
// Set a new source
function newSource(type, init) {
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
if (
!(type in types) ||
(!init && type === currentType) ||
(!currentType.length && type === types.video)
) {
return;
}
switch (type) {
case types.video:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
type: 'video/mp4',
size: 576,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
type: 'video/mp4',
size: 720,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
type: 'video/mp4',
size: 1080,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
type: 'video/mp4',
size: 1440,
},
],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
};
break;
case types.audio:
player.source = {
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3',
},
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg',
},
],
};
break;
case types.youtube:
player.source = {
type: 'video',
sources: [
{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
},
],
};
break;
case types.vimeo:
player.source = {
type: 'video',
sources: [
{
src: 'https://vimeo.com/76979871',
provider: 'vimeo',
},
],
};
break;
default:
break;
}
// Set the current type for next time
currentType = type;
function render(type) {
// Remove active classes // Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false)); Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
@@ -226,9 +86,27 @@ import Plyr from '../../../src/js/plyr';
// Show cite // Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => { Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
cite.setAttribute('hidden', ''); // eslint-disable-next-line no-param-reassign
cite.hidden = true;
}); });
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
document.querySelector(`.plyr__cite--${type}`).hidden = false;
}
// Set a new source
function setSource(type, init) {
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
if (!types.includes(type) || (!init && type === currentType) || (!currentType.length && type === 'video')) {
return;
}
// Set the new source
player.source = sources[type];
// Set the current type for next time
currentType = type;
render(type);
} }
// Bind to each button // Bind to each button
@@ -236,7 +114,7 @@ import Plyr from '../../../src/js/plyr';
button.addEventListener('click', () => { button.addEventListener('click', () => {
const type = button.getAttribute('data-source'); const type = button.getAttribute('data-source');
newSource(type); setSource(type);
if (historySupport) { if (historySupport) {
window.history.pushState({ type }, '', `#${type}`); window.history.pushState({ type }, '', `#${type}`);
@@ -246,42 +124,26 @@ import Plyr from '../../../src/js/plyr';
// List for backwards/forwards // List for backwards/forwards
window.addEventListener('popstate', event => { window.addEventListener('popstate', event => {
if (event.state && 'type' in event.state) { if (event.state && Object.keys(event.state).includes('type')) {
newSource(event.state.type); setSource(event.state.type);
} }
}); });
// On load
if (historySupport) {
const video = !currentType.length;
// If there's no current type set, assume video // If there's no current type set, assume video
if (video) { if (!hasInitialType) {
currentType = types.video; currentType = 'video';
} }
// Replace current history state // Replace current history state
if (currentType in types) { if (historySupport && types.includes(currentType)) {
window.history.replaceState( window.history.replaceState({ type: currentType }, '', hasInitialType ? `#${currentType}` : '');
{
type: currentType,
},
'',
video ? '' : `#${currentType}`,
);
} }
// If it's not video, load the source // If it's not video, load the source
if (currentType !== types.video) { if (currentType !== 'video') {
newSource(currentType, true); setSource(currentType, true);
} }
}
});
});
// Raven / Sentry render(currentType);
// For demo site (https://plyr.io) only });
if (env.prod) {
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
}
})(); })();
+81
View File
@@ -0,0 +1,81 @@
const sources = {
video: {
type: 'video',
title: 'View From A Blue Moon',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
type: 'video/mp4',
size: 576,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
type: 'video/mp4',
size: 720,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
type: 'video/mp4',
size: 1080,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
type: 'video/mp4',
size: 1440,
},
],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
previewThumbnails: {
src: ['https://cdn.plyr.io/static/demo/thumbs/100p.vtt', 'https://cdn.plyr.io/static/demo/thumbs/240p.vtt'],
},
},
audio: {
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3',
},
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg',
},
],
},
youtube: {
type: 'video',
sources: [
{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
},
],
},
vimeo: {
type: 'video',
sources: [
{
src: 'https://vimeo.com/40648169',
provider: 'vimeo',
},
],
},
};
export default sources;
+31
View File
@@ -0,0 +1,31 @@
// Setup tab focus
const container = document.getElementById('container');
const tabClassName = 'tab-focus';
// Remove class on blur
document.addEventListener('focusout', event => {
if (!event.target.classList || container.contains(event.target)) {
return;
}
event.target.classList.remove(tabClassName);
});
// Add classname to tabbed elements
document.addEventListener('keydown', event => {
if (event.keyCode !== 9) {
return;
}
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
const focused = document.activeElement;
if (!focused || !focused.classList || container.contains(focused)) {
return;
}
focused.classList.add(tabClassName);
}, 10);
});
+5
View File
@@ -0,0 +1,5 @@
// Toggle class on an element
const toggleClass = (element, className = '', toggle = false) =>
element && element.classList[toggle ? 'add' : 'remove'](className);
export default toggleClass;
+3
View File
@@ -3,6 +3,9 @@
// ========================================================================== // ==========================================================================
@charset 'UTF-8'; @charset 'UTF-8';
@import '../../../../src/sass/lib/css-vars';
$css-vars-use-native: true;
// Settings // Settings
@import '../settings/breakpoints'; @import '../settings/breakpoints';
@import '../settings/colors'; @import '../settings/colors';
+10 -9
View File
@@ -6,11 +6,9 @@
.button, .button,
.button__count { .button__count {
align-items: center; align-items: center;
background: $color-button-background;
border: 0; border: 0;
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 1px 1px rgba(#000, 0.1); box-shadow: 0 1px 1px rgba(#000, 0.1);
color: $color-button-text;
display: inline-flex; display: inline-flex;
padding: ($spacing-base * 0.75); padding: ($spacing-base * 0.75);
position: relative; position: relative;
@@ -21,14 +19,16 @@
// Buttons // Buttons
.button { .button {
background: $color-button-background;
color: $color-button-text;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
padding-left: $spacing-base; padding-left: ($spacing-base * 1.25);
padding-right: $spacing-base; padding-right: ($spacing-base * 1.25);
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover, &:hover,
&:focus { &:focus {
color: $gray-dark; background: $color-button-background-hover;
// Remove the underline/border // Remove the underline/border
&::after { &::after {
@@ -38,7 +38,6 @@
&:hover { &:hover {
box-shadow: 0 2px 2px rgba(#000, 0.1); box-shadow: 0 2px 2px rgba(#000, 0.1);
transform: translateY(-1px);
} }
&:focus { &:focus {
@@ -50,7 +49,7 @@
} }
&:active { &:active {
transform: translateY(1px); top: 1px;
} }
} }
@@ -66,12 +65,14 @@
// Count bubble // Count bubble
.button__count { .button__count {
animation: fadein 0.2s ease; animation: fadein 0.2s ease;
margin-left: ($spacing-base / 2); background: $color-button-count-background;
color: $color-button-count-text;
margin-left: ($spacing-base * 0.75);
&::before { &::before {
border: $arrow-size solid transparent; border: $arrow-size solid transparent;
border-left-width: 0; border-left-width: 0;
border-right-color: $color-button-background; border-right-color: $color-button-count-background;
content: ''; content: '';
height: 0; height: 0;
position: absolute; position: absolute;
+11
View File
@@ -6,6 +6,13 @@ header {
padding-bottom: $spacing-base; padding-bottom: $spacing-base;
text-align: center; text-align: center;
h1 span {
animation: shrinkHide 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) 2s forwards;
display: inline-block;
font-weight: $font-weight-light;
opacity: 0.5;
}
.call-to-action { .call-to-action {
margin-top: ($spacing-base * 1.5); margin-top: ($spacing-base * 1.5);
} }
@@ -15,5 +22,9 @@ header {
max-width: 360px; max-width: 360px;
padding-bottom: ($spacing-base * 2); padding-bottom: ($spacing-base * 2);
text-align: left; text-align: left;
p:first-of-type {
@include font-size($font-size-base + 1);
}
} }
} }
+1 -1
View File
@@ -19,5 +19,5 @@ label svg {
a .icon, a .icon,
.btn .icon { .btn .icon {
margin-right: floor($spacing-base / 3); margin-right: ($spacing-base / 2);
} }
-1
View File
@@ -12,7 +12,6 @@ button.faux-link {
a { a {
border-bottom: 1px dotted currentColor; border-bottom: 1px dotted currentColor;
color: $color-link; color: $color-link;
font-weight: $font-weight-bold;
position: relative; position: relative;
text-decoration: none; text-decoration: none;
transition: all 0.2s ease; transition: all 0.2s ease;
+2 -16
View File
@@ -2,16 +2,10 @@
// Examples // Examples
// ========================================================================== // ==========================================================================
// For non supported browsers
video {
max-width: 100%;
vertical-align: middle;
}
// Example players // Example players
.plyr { .plyr {
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 2px 5px rgba(#000, 0.2); box-shadow: 0 2px 15px rgba(#000, 0.1);
margin: $spacing-base auto; margin: $spacing-base auto;
&.plyr--audio { &.plyr--audio {
@@ -34,17 +28,9 @@ video {
// Style full supported player // Style full supported player
.plyr__cite { .plyr__cite {
display: none; color: $color-gray-500;
margin-top: $spacing-base;
.icon { .icon {
margin-right: ceil($spacing-base / 6); margin-right: ceil($spacing-base / 6);
} }
} }
.plyr--video:not(.plyr--youtube):not(.plyr--vimeo) ~ ul .plyr__cite--video,
.plyr--audio ~ ul .plyr__cite--audio,
.plyr--youtube ~ ul .plyr__cite--youtube,
.plyr--vimeo ~ ul .plyr__cite--vimeo {
display: block;
}
+1 -2
View File
@@ -35,11 +35,10 @@ main {
aside { aside {
align-items: center; align-items: center;
background: #fff; background: #fff;
color: $gray;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
justify-content: center; justify-content: center;
padding: ($spacing-base * 0.75); padding: $spacing-base;
position: relative; position: relative;
text-align: center; text-align: center;
text-shadow: none; text-shadow: none;
+14
View File
@@ -11,3 +11,17 @@
opacity: 1; opacity: 1;
} }
} }
@keyframes shrinkHide {
0% {
opacity: 0.5;
width: 38px;
}
20% {
width: 45px;
}
100% {
opacity: 0;
width: 0;
}
}
+1 -1
View File
@@ -36,7 +36,7 @@
@return #{$rem}rem; @return #{$rem}rem;
} }
@mixin font-size($size: 16) { @mixin font-size($size: $font-size-base) {
font-size: $size * 1px; // Fallback in px font-size: $size * 1px; // Fallback in px
font-size: calculate-rem($size); font-size: calculate-rem($size);
} }
+26 -16
View File
@@ -2,31 +2,41 @@
// Colors // Colors
// ========================================================================== // ==========================================================================
// Greyscale // Grayscale
$gray-dark: #343f4a; $color-gray-900: hsl(210, 15%, 16%);
$gray: #55646b; $color-gray-800: lighten($color-gray-900, 9%);
$gray-light: #cbd0d3; $color-gray-700: lighten($color-gray-800, 9%);
$gray-lighter: #dbe3e8; $color-gray-600: lighten($color-gray-700, 9%);
$off-white: #f2f5f7; $color-gray-500: lighten($color-gray-600, 9%);
$color-gray-400: lighten($color-gray-500, 9%);
$color-gray-300: lighten($color-gray-400, 9%);
$color-gray-200: lighten($color-gray-300, 9%);
$color-gray-100: lighten($color-gray-200, 9%);
$color-gray-50: lighten($color-gray-100, 9%);
// Branding
$color-brand-primary: hsl(198, 100%, 50%);
// Text // Text
$color-text: #fff; $color-text: $color-gray-700;
$color-headings: $color-brand-primary;
// Plyr
$color-brand-primary: #1aafff;
// Brands // Brands
$color-twitter: #4baaf4; $color-twitter: #4baaf4;
$color-youtube: #cc181e;
$color-vimeo: #19b7ed;
// Elements // Elements
$color-link: #fff; $color-link: $color-brand-primary;
$color-background: $color-brand-primary;
// Background
$color-background-from: hsl(198, 100%, 94%);
$color-background-to: hsl(198, 100%, 98%);
// Buttons // Buttons
$color-button-background: #fff; $color-button-background: $color-brand-primary;
$color-button-text: $gray; $color-button-text: #fff;
$color-button-background-hover: hsl(198, 100%, 55%);
$color-button-count-background: #fff;
$color-button-count-text: $color-gray-600;
// Focus // Focus
$tab-focus-default-color: #fff; $tab-focus-default-color: #fff;
+1 -1
View File
@@ -9,4 +9,4 @@ $arrow-size: 5px;
$border-radius-base: 4px; $border-radius-base: 4px;
// Background // Background
$page-background: linear-gradient(to left top, lighten($color-background, 10%), darken($color-background, 20%)); $page-background: linear-gradient(to left top, $color-background-from, $color-background-to);
+14 -18
View File
@@ -2,21 +2,17 @@
// Plyr Settings // Plyr Settings
// ========================================================================== // ==========================================================================
// Font @include css-vars(
$plyr-font-family: inherit; (
--plyr-color-main: $color-brand-primary,
// Sizes --plyr-font-size-base: 13px,
$plyr-font-size-base: 13px; --plyr-font-size-small: 12px,
$plyr-font-size-small: 12px; --plyr-font-size-time: 11px,
$plyr-font-size-time: 11px; --plyr-font-size-badges: 9px,
$plyr-font-size-badges: 9px; --plyr-font-size-menu: var(--plyr-font-size-base),
--plyr-font-weight-regular: 500,
// Other --plyr-font-weight-bold: 600,
$plyr-font-smoothing: true; --plyr-font-size-captions-medium: 18px,
--plyr-font-size-captions-large: 21px,
// Captions )
$plyr-font-size-captions-base: $plyr-font-size-base; );
$plyr-font-size-captions-small: $plyr-font-size-small;
$plyr-font-size-captions-medium: 18px;
$plyr-font-size-captions-large: 21px;
$plyr-font-size-menu: $plyr-font-size-base;
+1 -1
View File
@@ -2,4 +2,4 @@
// Colors // Colors
// ========================================================================== // ==========================================================================
$spacing-base: 20px; $spacing-base: 16px;
+1 -2
View File
@@ -14,7 +14,6 @@ body {
font-family: $font-sans-serif; font-family: $font-sans-serif;
font-weight: $font-weight-medium; font-weight: $font-weight-medium;
line-height: $line-height-base; line-height: $line-height-base;
text-shadow: 0 1px 1px rgba(#000, 0.15);
} }
button, button,
@@ -26,7 +25,7 @@ textarea {
p, p,
small { small {
margin: 0 0 $spacing-base; margin: 0 0 ($spacing-base * 1.5);
} }
small { small {
+2 -1
View File
@@ -4,8 +4,9 @@
h1 { h1 {
@include font-size($font-size-h1); @include font-size($font-size-h1);
color: $color-headings;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
letter-spacing: $letter-spacing-headings; letter-spacing: $letter-spacing-headings;
line-height: 1.2; line-height: 1.2;
margin: 0 0 $spacing-base; margin: 0 0 ($spacing-base * 1.5);
} }
+80
View File
@@ -0,0 +1,80 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@sentry/browser@^5.15.5":
version "5.15.5"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.15.5.tgz#d9a51f1388581067b50d30ed9b1aed2cbb333a36"
integrity sha512-rqDvjk/EvogfdbZ4TiEpxM/lwpPKmq23z9YKEO4q81+1SwJNua53H60dOk9HpRU8nOJ1g84TMKT2Ov8H7sqDWA==
dependencies:
"@sentry/core" "5.15.5"
"@sentry/types" "5.15.5"
"@sentry/utils" "5.15.5"
tslib "^1.9.3"
"@sentry/core@5.15.5":
version "5.15.5"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.15.5.tgz#40ea79bff5272d3fbbeeb4a98cdc59e1adbd2c92"
integrity sha512-enxBLv5eibBMqcWyr+vApqeix8uqkfn0iGsD3piKvoMXCgKsrfMwlb/qo9Ox0lKr71qIlZVt+9/A2vZohdgnlg==
dependencies:
"@sentry/hub" "5.15.5"
"@sentry/minimal" "5.15.5"
"@sentry/types" "5.15.5"
"@sentry/utils" "5.15.5"
tslib "^1.9.3"
"@sentry/hub@5.15.5":
version "5.15.5"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.15.5.tgz#f5abbcdbe656a70e2ff02c02a5a4cffa0f125935"
integrity sha512-zX9o49PcNIVMA4BZHe//GkbQ4Jx+nVofqU/Il32/IbwKhcpPlhGX3c1sOVQo4uag3cqd/JuQsk+DML9TKkN0Lw==
dependencies:
"@sentry/types" "5.15.5"
"@sentry/utils" "5.15.5"
tslib "^1.9.3"
"@sentry/minimal@5.15.5":
version "5.15.5"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.15.5.tgz#a0e4e071f01d9c4d808094ae7203f6c4cca9348a"
integrity sha512-zQkkJ1l9AjmU/Us5IrOTzu7bic4sTPKCatptXvLSTfyKW7N6K9MPIIFeSpZf9o1yM2sRYdK7GV08wS2eCT3JYw==
dependencies:
"@sentry/hub" "5.15.5"
"@sentry/types" "5.15.5"
tslib "^1.9.3"
"@sentry/types@5.15.5":
version "5.15.5"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.15.5.tgz#16c97e464cf09bbd1d2e8ce90d130e781709076e"
integrity sha512-F9A5W7ucgQLJUG4LXw1ZIy4iLevrYZzbeZ7GJ09aMlmXH9PqGThm1t5LSZlVpZvUfQ2rYA8NU6BdKJSt7B5LPw==
"@sentry/utils@5.15.5":
version "5.15.5"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.15.5.tgz#dec1d4c79037c4da08b386f5d34409234dcbfb15"
integrity sha512-Nl9gl/MGnzSkuKeo3QaefoD/OJrFLB8HmwQ7HUbTXb6E7yyEzNKAQMHXGkwNAjbdYyYbd42iABP6Y5F/h39NtA==
dependencies:
"@sentry/types" "5.15.5"
tslib "^1.9.3"
core-js@^3.6.5:
version "3.6.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
custom-event-polyfill@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee"
integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
shr-buttons@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/shr-buttons/-/shr-buttons-2.0.3.tgz#2ffd021fc3d789e1510ce2736b938bd09ea1da5a"
integrity sha512-sPAgHiw4uaIt9TnxTfyZEedDChcldSVtnBHE44cpe/mSC7rqm4IEKZRLYqnVlTcGM+FSDNBPUNpSf50Q2ntd+w==
tslib@^1.9.3:
version "1.11.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
url-polyfill@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.8.tgz#21eb58ad61192f52b77dcac8ab5293ae7bc67060"
integrity sha512-Ey61F4FEqhcu1vHSOMmjl0Vd/RPRLEjMj402qszD/dhMBrVfoUsnIj8KSZo2yj+eIlxJGKFdnm6ES+7UzMgZ3Q==
+1 -1
View File
File diff suppressed because one or more lines are too long
+1570 -1159
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1569 -1158
View File
File diff suppressed because it is too large Load Diff
+7128 -3684
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+7127 -3683
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

+2 -475
View File
@@ -1,481 +1,8 @@
// ========================================================================== // ==========================================================================
// Gulp build script // Gulp build script
// ========================================================================== // ==========================================================================
/* global require, __dirname */
/* eslint no-console: "off" */
const path = require('path');
const gulp = require('gulp'); const gulp = require('gulp');
const HubRegistry = require('gulp-hub');
// JavaScript gulp.registry(new HubRegistry(['tasks/*.js']));
const terser = require('gulp-terser');
const rollup = require('gulp-better-rollup');
const babel = require('rollup-plugin-babel');
const commonjs = require('rollup-plugin-commonjs');
const resolve = require('rollup-plugin-node-resolve');
// CSS
const sass = require('gulp-sass');
const clean = require('gulp-clean-css');
const prefix = require('gulp-autoprefixer');
// Images
const svgstore = require('gulp-svgstore');
const imagemin = require('gulp-imagemin');
// Utils
const del = require('del');
const filter = require('gulp-filter');
const header = require('gulp-header');
const gitbranch = require('git-branch');
const rename = require('gulp-rename');
const replace = require('gulp-replace');
const ansi = require('ansi-colors');
const log = require('fancy-log');
const open = require('gulp-open');
const plumber = require('gulp-plumber');
const size = require('gulp-size');
const sourcemaps = require('gulp-sourcemaps');
const through = require('through2');
// Deployment
const aws = require('aws-sdk');
const publish = require('gulp-awspublish');
const FastlyPurge = require('fastly-purge');
const pkg = require('./package.json');
const build = require('./build.json');
const deploy = require('./deploy.json');
const { browserslist: browsers, version } = pkg;
const minSuffix = '.min';
// Get AWS config
Object.values(deploy).forEach(target => {
Object.assign(target, {
publisher: publish.create({
region: target.region,
params: {
Bucket: target.bucket,
},
credentials: new aws.SharedIniFileCredentials({ profile: 'plyr' }),
}),
});
});
// Paths
const paths = {
plyr: {
// Source paths
src: {
sass: path.join(__dirname, 'src/sass/**/*.scss'),
js: path.join(__dirname, 'src/js/**/*.js'),
sprite: path.join(__dirname, 'src/sprite/*.svg'),
},
// Output paths
output: path.join(__dirname, 'dist/'),
},
demo: {
// Source paths
src: {
sass: path.join(__dirname, 'demo/src/sass/**/*.scss'),
js: path.join(__dirname, 'demo/src/js/**/*.js'),
},
// Output paths
output: path.join(__dirname, 'demo/dist/'),
// Demo
root: path.join(__dirname, 'demo/'),
},
upload: [
path.join(__dirname, `dist/*${minSuffix}.*`),
path.join(__dirname, 'dist/*.css'),
path.join(__dirname, 'dist/*.svg'),
path.join(__dirname, `demo/dist/*${minSuffix}.*`),
path.join(__dirname, 'demo/dist/*.css'),
path.join(__dirname, 'demo/dist/*.svg'),
],
};
// Task arrays
const tasks = {
css: [],
js: [],
sprite: [],
clean: 'clean',
};
// Size plugin
const sizeOptions = { showFiles: true, gzip: true };
// Clean out /dist
gulp.task(tasks.clean, done => {
const dirs = [paths.plyr.output, paths.demo.output].map(dir => path.join(dir, '**/*'));
// Don't delete the mp4
dirs.push(`!${path.join(paths.plyr.output, '**/*.mp4')}`);
del(dirs);
done();
});
// JavaScript
Object.entries(build.js).forEach(([filename, entry]) => {
entry.formats.forEach(format => {
const name = `js:${filename}:${format}`;
tasks.js.push(name);
const polyfill = filename.includes('polyfilled');
const extension = format === 'es' ? 'mjs' : 'js';
gulp.task(name, () =>
gulp
.src(entry.src)
.pipe(plumber())
.pipe(sourcemaps.init())
.pipe(
rollup(
{
plugins: [
resolve(),
commonjs(),
babel({
presets: [
[
'@babel/env',
{
// debug: true,
useBuiltIns: polyfill ? 'usage' : false,
},
],
],
babelrc: false,
exclude: [/\/core-js\//],
}),
],
},
{
name: entry.namespace,
format,
},
),
)
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
.pipe(
rename({
extname: `.${extension}`,
}),
)
.pipe(gulp.dest(entry.dist))
.pipe(filter(`**/*.${extension}`))
.pipe(terser())
.pipe(rename({ suffix: minSuffix }))
.pipe(size(sizeOptions))
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(entry.dist)),
);
});
});
// CSS
Object.entries(build.css).forEach(([filename, entry]) => {
const name = `css:${filename}`;
tasks.css.push(name);
gulp.task(name, () =>
gulp
.src(entry.src)
.pipe(plumber())
.pipe(sass())
.pipe(
prefix(browsers, {
cascade: false,
}),
)
.pipe(clean())
.pipe(size(sizeOptions))
.pipe(gulp.dest(entry.dist)),
);
});
// SVG Sprites
Object.entries(build.sprite).forEach(([filename, entry]) => {
const name = `sprite:${filename}`;
tasks.sprite.push(name);
gulp.task(name, () =>
gulp
.src(entry.src)
.pipe(plumber())
.pipe(imagemin())
.pipe(svgstore())
.pipe(rename({ basename: path.parse(filename).name }))
.pipe(size(sizeOptions))
.pipe(gulp.dest(entry.dist)),
);
});
// Build all JS
gulp.task('js', () => gulp.parallel(...tasks.js));
// Watch for file changes
gulp.task('watch', () => {
// Plyr core
gulp.watch(paths.plyr.src.js, gulp.parallel(...tasks.js));
gulp.watch(paths.plyr.src.sass, gulp.parallel(...tasks.css));
gulp.watch(paths.plyr.src.sprite, gulp.parallel(...tasks.sprite));
// Demo
gulp.watch(paths.demo.src.js, gulp.parallel(...tasks.js));
gulp.watch(paths.demo.src.sass, gulp.parallel(...tasks.css));
});
// Build distribution
gulp.task('build', gulp.series(tasks.clean, gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite)));
// Default gulp task
gulp.task('default', gulp.series('build', 'watch'));
// Publish a version to CDN and demo
// --------------------------------------------
// Get deployment config
let credentials = {};
try {
credentials = require('./credentials.json'); //eslint-disable-line
} catch (e) {
// Do nothing
}
// Get branch info
const branch = {
current: gitbranch.sync(),
master: 'master',
beta: 'beta',
};
const maxAge = 31536000; // 1 year
const options = {
cdn: {
headers: {
'Cache-Control': `max-age=${maxAge}`,
},
},
demo: {
uploadPath: branch.current === branch.beta ? 'beta' : null,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
},
},
symlinks(ver, filename) {
return {
headers: {
// http://stackoverflow.com/questions/2272835/amazon-s3-object-redirect
'x-amz-website-redirect-location': `/${ver}/${filename}`,
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
},
};
},
};
const regex =
'(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*).(?:0|[1-9][0-9]*)(?:-[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?(?:\\+[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?';
const semver = new RegExp(`v${regex}`, 'gi');
const localPath = new RegExp('(../)?dist', 'gi');
const versionPath = `https://${deploy.cdn.domain}/${version}`;
const cdnpath = new RegExp(`${deploy.cdn.domain}/${regex}/`, 'gi');
const renameFile = rename(p => {
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
});
// Check we're on the correct branch to deploy
const canDeploy = () => {
const allowed = [branch.master, branch.beta];
if (!allowed.includes(branch.current)) {
console.error(`Must be on ${allowed.join(', ')} to publish! (current: ${branch.current})`);
return false;
}
return true;
};
gulp.task('version', done => {
if (!canDeploy()) {
done();
return null;
}
const { domain } = deploy.cdn;
log(`Uploading ${ansi.green.bold(version)} to ${ansi.cyan(domain)}...`);
// Replace versioned URLs in source
const files = ['plyr.js', 'plyr.polyfilled.js', 'config/defaults.js'];
return gulp
.src(files.map(file => path.join(__dirname, `src/js/${file}`)), { base: '.' })
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${domain}/${version}/`))
.pipe(gulp.dest('./'));
});
// Publish version to CDN bucket
gulp.task('cdn', done => {
if (!canDeploy()) {
done();
return null;
}
const { domain, publisher } = deploy.cdn;
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
log(`Uploading ${ansi.green.bold(pkg.version)} to ${ansi.cyan(domain)}...`);
// Upload to CDN
return (
gulp
.src(paths.upload)
.pipe(renameFile)
// Remove min suffix from source map URL
.pipe(
replace(
/sourceMappingURL=([\w-?.]+)/,
(match, filename) => `sourceMappingURL=${filename.replace(minSuffix, '')}`,
),
)
.pipe(size(sizeOptions))
.pipe(replace(localPath, versionPath))
.pipe(publisher.publish(options.cdn.headers))
.pipe(publish.reporter())
);
});
// Purge the fastly cache incase any 403/404 are cached
gulp.task('purge', () => {
if (!Object.keys(credentials).includes('fastly')) {
throw new Error('Fastly credentials required to purge cache.');
}
const { fastly } = credentials;
const list = [];
return gulp
.src(paths.upload)
.pipe(
through.obj((file, enc, cb) => {
const filename = file.path.split('/').pop();
list.push(`${versionPath}/${filename.replace(minSuffix, '')}`);
cb(null);
}),
)
.on('end', () => {
const purge = new FastlyPurge(fastly.token);
list.forEach(url => {
log(`Purging ${ansi.cyan(url)}...`);
purge.url(url, (error, result) => {
if (error) {
log.error(error);
} else if (result) {
log(result);
}
});
});
});
});
// Publish to demo bucket
gulp.task('demo', done => {
if (!canDeploy()) {
done();
return null;
}
const { publisher } = deploy.demo;
const { domain } = deploy.cdn;
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
log(`Uploading ${ansi.green.bold(pkg.version)} to ${ansi.cyan(domain)}...`);
// Replace versioned files in readme.md
gulp.src([`${__dirname}/readme.md`])
.pipe(replace(cdnpath, `${domain}/${version}/`))
.pipe(gulp.dest(__dirname));
// Replace local file paths with remote paths in demo HTML
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
const index = `${paths.demo.root}index.html`;
const error = `${paths.demo.root}error.html`;
const pages = [index];
if (branch.current === branch.master) {
pages.push(error);
}
return gulp
.src(pages)
.pipe(replace(localPath, versionPath))
.pipe(publisher.publish(options.demo.headers))
.pipe(publish.reporter());
});
gulp.task('error', done => {
// Only update CDN for master (prod)
if (!canDeploy() || branch.current !== branch.master) {
done();
return null;
}
const { publisher } = deploy.cdn;
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
// Replace local file paths with remote paths in demo HTML
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
// Upload error.html to cdn
return gulp
.src(`${paths.demo.root}error.html`)
.pipe(replace(localPath, versionPath))
.pipe(publisher.publish(options.demo.headers))
.pipe(publish.reporter());
});
// Open the demo site to check it's ok
gulp.task('open', () => {
const { domain } = deploy.demo;
return gulp.src(__filename).pipe(
open({
uri: `https://${domain}/${branch.current === branch.beta ? 'beta' : ''}`,
}),
);
});
// Do everything
gulp.task(
'deploy',
gulp.series(
'version',
tasks.clean,
gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite),
'cdn',
'demo',
'purge',
'open',
),
);
+50 -44
View File
@@ -1,10 +1,11 @@
{ {
"name": "plyr", "name": "plyr",
"version": "3.5.2", "version": "3.6.2",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player", "description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io", "homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>", "author": "Sam Potts <sam@potts.es>",
"main": "dist/plyr.js", "main": "dist/plyr.js",
"types": "src/js/plyr.d.ts",
"module": "dist/plyr.min.mjs", "module": "dist/plyr.min.mjs",
"jsnext:main": "dist/plyr.min.mjs", "jsnext:main": "dist/plyr.min.mjs",
"browser": "dist/plyr.min.js", "browser": "dist/plyr.min.js",
@@ -31,64 +32,69 @@
"scripts": { "scripts": {
"build": "gulp build", "build": "gulp build",
"lint": "eslint src/js && npm run-script remark", "lint": "eslint src/js && npm run-script remark",
"lint:fix": "eslint --fix src/js",
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'", "remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
"deploy": "yarn lint && gulp deploy" "deploy": "yarn lint && gulp version && gulp build && gulp deploy",
"format": "prettier --write \"./{src,demo/src}/**/*.{js,scss}\""
}, },
"devDependencies": { "devDependencies": {
"ansi-colors": "^3.2.3", "ansi-colors": "^4.1.1",
"aws-sdk": "^2.409.0", "autoprefixer": "^9.7.6",
"@babel/core": "^7.3.3", "aws-sdk": "^2.668.0",
"@babel/preset-env": "^7.3.1", "@babel/core": "^7.9.6",
"babel-eslint": "^10.0.1", "@babel/preset-env": "^7.9.6",
"del": "^3.0.0", "babel-eslint": "^10.1.0",
"eslint": "^5.14.1", "browser-sync": "^2.26.7",
"eslint-config-airbnb-base": "^13.1.0", "del": "^5.1.0",
"eslint-config-prettier": "^4.0.0", "eslint": "^6.8.0",
"eslint-plugin-import": "^2.16.0", "eslint-config-airbnb-base": "^14.1.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-simple-import-sort": "^5.0.3",
"fancy-log": "^1.3.3", "fancy-log": "^1.3.3",
"fastly-purge": "^1.0.1", "fastly-purge": "^1.0.1",
"git-branch": "^2.0.1", "git-branch": "^2.0.1",
"gulp": "^4.0.0", "gulp": "^4.0.2",
"gulp-autoprefixer": "^6.0.0", "gulp-awspublish": "^4.1.1",
"gulp-awspublish": "^4.0.0", "gulp-better-rollup": "^4.0.1",
"gulp-better-rollup": "^3.4.0", "gulp-filter": "^6.0.0",
"gulp-clean-css": "^4.0.0", "gulp-header": "^2.0.9",
"gulp-filter": "^5.1.0", "gulp-hub": "^4.2.0",
"gulp-header": "^2.0.7", "gulp-imagemin": "^7.1.0",
"gulp-imagemin": "^5.0.3",
"gulp-open": "^3.0.1", "gulp-open": "^3.0.1",
"gulp-plumber": "^1.2.1", "gulp-plumber": "^1.2.1",
"gulp-postcss": "^8.0.0", "gulp-postcss": "^8.0.0",
"gulp-rename": "^1.4.0", "gulp-rename": "^2.0.0",
"gulp-replace": "^1.0.0", "gulp-replace": "^1.0.0",
"gulp-sass": "^4.0.2", "gulp-sass": "^4.1.0",
"gulp-size": "^3.0.0", "gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.5", "gulp-sourcemaps": "^2.6.5",
"gulp-svgstore": "^7.0.1", "gulp-svgstore": "^7.0.1",
"gulp-terser": "^1.1.7", "gulp-terser": "^1.2.0",
"postcss-custom-properties": "^8.0.9", "postcss-clean": "^1.1.0",
"prettier-eslint": "^8.8.2", "postcss-custom-properties": "^9.1.1",
"prettier-eslint": "^9.0.1",
"prettier-stylelint": "^0.4.2", "prettier-stylelint": "^0.4.2",
"remark-cli": "^6.0.1", "remark-cli": "^8.0.0",
"remark-validate-links": "^8.0.0", "remark-validate-links": "^10.0.0",
"rollup-plugin-babel": "^4.3.2", "rollup": "^2.7.6",
"rollup-plugin-commonjs": "^9.2.1", "rollup-plugin-babel": "^4.4.0",
"rollup-plugin-node-resolve": "^4.0.1", "rollup-plugin-commonjs": "^10.1.0",
"stylelint": "^9.10.1", "rollup-plugin-node-resolve": "^5.2.0",
"stylelint-config-prettier": "^5.0.0", "stylelint": "^13.3.3",
"stylelint-config-recommended": "^2.1.0", "stylelint-config-prettier": "^8.0.1",
"stylelint-config-sass-guidelines": "^5.3.0", "stylelint-config-recommended": "^3.0.0",
"stylelint-order": "^2.0.0", "stylelint-config-sass-guidelines": "^7.0.0",
"stylelint-scss": "^3.5.4", "stylelint-order": "^4.0.0",
"stylelint-selector-bem-pattern": "^2.0.0", "stylelint-scss": "^3.17.1",
"through2": "^3.0.0" "stylelint-selector-bem-pattern": "^2.1.0",
"through2": "^3.0.1"
}, },
"dependencies": { "dependencies": {
"core-js": "^2.6.5", "core-js": "^3.6.5",
"custom-event-polyfill": "^1.0.6", "custom-event-polyfill": "^1.0.7",
"loadjs": "^3.5.5", "loadjs": "^4.2.0",
"rangetouch": "^2.0.0", "rangetouch": "^2.0.1",
"raven-js": "^3.27.0", "url-polyfill": "^1.1.8"
"url-polyfill": "^1.1.3"
} }
} }
+17 -9
View File
@@ -5,25 +5,33 @@
} }
], ],
"settings": { "settings": {
"search.exclude": {
"**/node_modules": true,
"**/dist": true
},
// Linting // Linting
"stylelint.enable": true, "stylelint.enable": true,
"css.validate": false, "css.validate": false,
"scss.validate": false, "scss.validate": false,
"javascript.validate.enable": false, "javascript.validate.enable": false,
// Prettier
"prettier.eslintIntegration": true,
"prettier.stylelintIntegration": true,
// Formatting // Formatting
"editor.tabSize": 4, "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.insertSpaces": true, "editor.insertSpaces": true,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
// Trim on save // Trim on save
"files.trimTrailingWhitespace": true "files.trimTrailingWhitespace": true,
// Special file associations
"files.associations": {
".eslintrc": "jsonc"
},
"editor.codeActionsOnSave": {
"source.fixAll": true
}
} }
} }
+27 -5
View File
@@ -85,7 +85,6 @@ const captions = {
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
const languages = dedupe(browserLanguages.map(language => language.split('-')[0])); const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
// Use first browser language when language is 'auto' // Use first browser language when language is 'auto'
@@ -124,15 +123,24 @@ const captions = {
// Handle tracks (add event listener and "pseudo"-default) // Handle tracks (add event listener and "pseudo"-default)
if (this.isHTML5 && this.isVideo) { if (this.isHTML5 && this.isVideo) {
tracks.filter(track => !meta.get(track)).forEach(track => { tracks
.filter(track => !meta.get(track))
.forEach(track => {
this.debug.log('Track added', track); this.debug.log('Track added', track);
// Attempt to store if the original dom element was "default" // Attempt to store if the original dom element was "default"
meta.set(track, { meta.set(track, {
default: track.mode === 'showing', default: track.mode === 'showing',
}); });
// Turn off native caption rendering to avoid double captions // Turn off native caption rendering to avoid double captions
// Note: mode='hidden' forces a track to download. To ensure every track
// isn't downloaded at once, only 'showing' tracks should be reassigned
// eslint-disable-next-line no-param-reassign
if (track.mode === 'showing') {
// eslint-disable-next-line no-param-reassign
track.mode = 'hidden'; track.mode = 'hidden';
}
// Add event listener for cue changes // Add event listener for cue changes
on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
@@ -149,7 +157,11 @@ const captions = {
toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks)); toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
// Update available languages in list // Update available languages in list
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) { if (
is.array(this.config.controls) &&
this.config.controls.includes('settings') &&
this.config.settings.includes('captions')
) {
controls.setCaptionsMenu.call(this); controls.setCaptionsMenu.call(this);
} }
}, },
@@ -164,7 +176,6 @@ const captions = {
const { toggled } = this.captions; // Current state const { toggled } = this.captions; // Current state
const activeClass = this.config.classNames.captions.active; const activeClass = this.config.classNames.captions.active;
// Get the next state // Get the next state
// If the method is called without parameter, toggle based on current value // If the method is called without parameter, toggle based on current value
const active = is.nullOrUndefined(input) ? !toggled : input; const active = is.nullOrUndefined(input) ? !toggled : input;
@@ -206,6 +217,14 @@ const captions = {
// Trigger event (not used internally) // Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
} }
// Wait for the call stack to clear before setting mode='hidden'
// on the active track - forcing the browser to download it
setTimeout(() => {
if (active && this.captions.toggled) {
this.captions.currentTrackNode.mode = 'hidden';
}
});
}, },
// Set captions by track index // Set captions by track index
@@ -300,10 +319,12 @@ const captions = {
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default); const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a)); const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
let track; let track;
languages.every(language => { languages.every(language => {
track = sorted.find(track => track.language === language); track = sorted.find(t => t.language === language);
return !track; // Break iteration if there is a match return !track; // Break iteration if there is a match
}); });
// If no match is found but is required, get first // If no match is found but is required, get first
return track || (force ? sorted[0] : undefined); return track || (force ? sorted[0] : undefined);
}, },
@@ -360,6 +381,7 @@ const captions = {
// Get cues from track // Get cues from track
if (!cues) { if (!cues) {
const track = captions.getCurrentTrack.call(this); const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || []) cues = Array.from((track || {}).activeCues || [])
.map(cue => cue.getCueAsHTML()) .map(cue => cue.getCueAsHTML())
.map(getHTML); .map(getHTML);
+22 -15
View File
@@ -42,8 +42,9 @@ const defaults = {
// Clicking the currentTime inverts it's value to show time left rather than elapsed // Clicking the currentTime inverts it's value to show time left rather than elapsed
toggleInvert: true, toggleInvert: true,
// Aspect ratio (for embeds) // Force an aspect ratio
ratio: '16:9', // The format must be `'w:h'` (e.g. `'16:9'`)
ratio: null,
// Click video container to play/pause // Click video container to play/pause
clickToPlay: true, clickToPlay: true,
@@ -60,7 +61,7 @@ const defaults = {
// Sprite (for icons) // Sprite (for icons)
loadSprite: true, loadSprite: true,
iconPrefix: 'plyr', iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.5.2/plyr.svg', iconUrl: 'https://cdn.plyr.io/3.6.2/plyr.svg',
// Blank video (used to prevent errors on source change) // Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4', blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@@ -68,7 +69,10 @@ const defaults = {
// Quality default // Quality default
quality: { quality: {
default: 576, default: 576,
// The options to display in the UI, if available for the source media
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240], options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
forced: false,
onChange: null,
}, },
// Set loops // Set loops
@@ -81,7 +85,8 @@ const defaults = {
// Speed default and options to display // Speed default and options to display
speed: { speed: {
selected: 1, selected: 1,
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],
}, },
// Keyboard shortcut settings // Keyboard shortcut settings
@@ -110,6 +115,9 @@ const defaults = {
enabled: true, // Allow fullscreen? enabled: true, // Allow fullscreen?
fallback: true, // Fallback using full viewport/window fallback: true, // Fallback using full viewport/window
iosNative: false, // Use the native fullscreen in iOS (disables custom controls) iosNative: false, // Use the native fullscreen in iOS (disables custom controls)
// Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode
// Non-ancestors of the player element will be ignored
// container: null, // defaults to the player element
}, },
// Local storage // Local storage
@@ -127,6 +135,7 @@ const defaults = {
// 'fast-forward', // 'fast-forward',
'progress', 'progress',
'current-time', 'current-time',
// 'duration',
'mute', 'mute',
'volume', 'volume',
'captions', 'captions',
@@ -162,6 +171,7 @@ const defaults = {
frameTitle: 'Player for {title}', frameTitle: 'Player for {title}',
captions: 'Captions', captions: 'Captions',
settings: 'Settings', settings: 'Settings',
pip: 'PIP',
menuBack: 'Go back to previous menu', menuBack: 'Go back to previous menu',
speed: 'Speed', speed: 'Speed',
normal: 'Normal', normal: 'Normal',
@@ -194,8 +204,7 @@ const defaults = {
}, },
youtube: { youtube: {
sdk: 'https://www.youtube.com/iframe_api', sdk: 'https://www.youtube.com/iframe_api',
api: api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',
'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
}, },
googleIMA: { googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
@@ -319,9 +328,6 @@ const defaults = {
progress: '.plyr__progress', progress: '.plyr__progress',
captions: '.plyr__captions', captions: '.plyr__captions',
caption: '.plyr__caption', caption: '.plyr__caption',
menu: {
quality: '.js-plyr__menu__list--quality',
},
}, },
// Class hooks added to the player in different states // Class hooks added to the player in different states
@@ -330,6 +336,7 @@ const defaults = {
provider: 'plyr--{0}', provider: 'plyr--{0}',
video: 'plyr__video-wrapper', video: 'plyr__video-wrapper',
embed: 'plyr__video-embed', embed: 'plyr__video-embed',
videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',
embedContainer: 'plyr__video-embed__container', embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster', poster: 'plyr__poster',
posterEnabled: 'plyr__poster-enabled', posterEnabled: 'plyr__poster-enabled',
@@ -394,11 +401,6 @@ const defaults = {
}, },
}, },
// API keys
keys: {
google: null,
},
// Advertisements plugin // Advertisements plugin
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: { ads: {
@@ -420,11 +422,16 @@ const defaults = {
title: false, title: false,
speed: true, speed: true,
transparent: false, transparent: false,
// Whether the owner of the video has a Pro or Business account
// (which allows us to properly hide controls without CSS hacks, etc)
premium: false,
// Custom settings from Plyr
referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
}, },
// YouTube plugin // YouTube plugin
youtube: { youtube: {
noCookie: false, // Whether to use an alternative version of YouTube without cookies noCookie: true, // Whether to use an alternative version of YouTube without cookies
rel: 0, // No related vids rel: 0, // No related vids
showinfo: 0, // Hide info showinfo: 0, // Hide info
iv_load_policy: 3, // Hide annotations iv_load_policy: 3, // Hide annotations
+144 -115
View File
@@ -4,6 +4,7 @@
// ========================================================================== // ==========================================================================
import RangeTouch from 'rangetouch'; import RangeTouch from 'rangetouch';
import captions from './captions'; import captions from './captions';
import html5 from './html5'; import html5 from './html5';
import support from './support'; import support from './support';
@@ -27,7 +28,7 @@ import {
import { off, on } from './utils/events'; import { off, on } from './utils/events';
import i18n from './utils/i18n'; import i18n from './utils/i18n';
import is from './utils/is'; import is from './utils/is';
import loadSprite from './utils/loadSprite'; import loadSprite from './utils/load-sprite';
import { extend } from './utils/objects'; import { extend } from './utils/objects';
import { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings'; import { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';
import { formatTime, getHours } from './utils/time'; import { formatTime, getHours } from './utils/time';
@@ -83,9 +84,7 @@ const controls = {
// Seek tooltip // Seek tooltip
if (is.element(this.elements.progress)) { if (is.element(this.elements.progress)) {
this.elements.display.seekTooltip = this.elements.progress.querySelector( this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
`.${this.config.classNames.tooltip}`,
);
} }
return true; return true;
@@ -105,13 +104,12 @@ const controls = {
const namespace = 'http://www.w3.org/2000/svg'; const namespace = 'http://www.w3.org/2000/svg';
const iconUrl = controls.getIconUrl.call(this); const iconUrl = controls.getIconUrl.call(this);
const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`; const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;
// Create <svg> // Create <svg>
const icon = document.createElementNS(namespace, 'svg'); const icon = document.createElementNS(namespace, 'svg');
setAttributes( setAttributes(
icon, icon,
extend(attributes, { extend(attributes, {
role: 'presentation', 'aria-hidden': 'true',
focusable: 'false', focusable: 'false',
}), }),
); );
@@ -139,10 +137,7 @@ const controls = {
// Create hidden text label // Create hidden text label
createLabel(key, attr = {}) { createLabel(key, attr = {}) {
const text = i18n.get(key, this.config); const text = i18n.get(key, this.config);
const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') };
const attributes = Object.assign({}, attr, {
class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' '),
});
return createElement('span', attributes, text); return createElement('span', attributes, text);
}, },
@@ -172,7 +167,7 @@ const controls = {
// Create a <button> // Create a <button>
createButton(buttonType, attr) { createButton(buttonType, attr) {
const attributes = Object.assign({}, attr); const attributes = extend({}, attr);
let type = toCamelCase(buttonType); let type = toCamelCase(buttonType);
const props = { const props = {
@@ -198,8 +193,10 @@ const controls = {
// Set class name // Set class name
if (Object.keys(attributes).includes('class')) { if (Object.keys(attributes).includes('class')) {
if (!attributes.class.includes(this.config.classNames.control)) { if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {
attributes.class += ` ${this.config.classNames.control}`; extend(attributes, {
class: `${attributes.class} ${this.config.classNames.control}`,
});
} }
} else { } else {
attributes.class = this.config.classNames.control; attributes.class = this.config.classNames.control;
@@ -377,13 +374,13 @@ const controls = {
}, },
// Create time display // Create time display
createTime(type) { createTime(type, attrs) {
const attributes = getAttributesFromSelector(this.config.selectors.display[type]); const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);
const container = createElement( const container = createElement(
'div', 'div',
extend(attributes, { extend(attributes, {
class: `${this.config.classNames.display.time} ${attributes.class ? attributes.class : ''}`.trim(), class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),
'aria-label': i18n.get(type, this.config), 'aria-label': i18n.get(type, this.config),
}), }),
'00:00', '00:00',
@@ -400,7 +397,8 @@ const controls = {
// https://bugzilla.mozilla.org/show_bug.cgi?id=1220143 // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143
bindMenuItemShortcuts(menuItem, type) { bindMenuItemShortcuts(menuItem, type) {
// Navigate through menus via arrow keys and space // Navigate through menus via arrow keys and space
on( on.call(
this,
menuItem, menuItem,
'keydown keyup', 'keydown keyup',
event => { event => {
@@ -450,7 +448,7 @@ const controls = {
// Enter will fire a `click` event but we still need to manage focus // Enter will fire a `click` event but we still need to manage focus
// So we bind to keyup which fires after and set focus here // So we bind to keyup which fires after and set focus here
on(menuItem, 'keyup', event => { on.call(this, menuItem, 'keyup', event => {
if (event.which !== 13) { if (event.which !== 13) {
return; return;
} }
@@ -491,15 +489,15 @@ const controls = {
get() { get() {
return menuItem.getAttribute('aria-checked') === 'true'; return menuItem.getAttribute('aria-checked') === 'true';
}, },
set(checked) { set(check) {
// Ensure exclusivity // Ensure exclusivity
if (checked) { if (check) {
Array.from(menuItem.parentNode.children) Array.from(menuItem.parentNode.children)
.filter(node => matches(node, '[role="menuitemradio"]')) .filter(node => matches(node, '[role="menuitemradio"]'))
.forEach(node => node.setAttribute('aria-checked', 'false')); .forEach(node => node.setAttribute('aria-checked', 'false'));
} }
menuItem.setAttribute('aria-checked', checked ? 'true' : 'false'); menuItem.setAttribute('aria-checked', check ? 'true' : 'false');
}, },
}); });
@@ -607,17 +605,17 @@ const controls = {
let value = 0; let value = 0;
const setProgress = (target, input) => { const setProgress = (target, input) => {
const value = is.number(input) ? input : 0; const val = is.number(input) ? input : 0;
const progress = is.element(target) ? target : this.elements.display.buffer; const progress = is.element(target) ? target : this.elements.display.buffer;
// Update value and label // Update value and label
if (is.element(progress)) { if (is.element(progress)) {
progress.value = value; progress.value = val;
// Update text label inside // Update text label inside
const label = progress.getElementsByTagName('span')[0]; const label = progress.getElementsByTagName('span')[0];
if (is.element(label)) { if (is.element(label)) {
label.childNodes[0].nodeValue = value; label.childNodes[0].nodeValue = val;
} }
} }
}; };
@@ -699,14 +697,8 @@ const controls = {
return; return;
} }
// Calculate percentage
let percent = 0;
const clientRect = this.elements.progress.getBoundingClientRect();
const visible = `${this.config.classNames.tooltip}--visible`; const visible = `${this.config.classNames.tooltip}--visible`;
const toggle = show => toggleClass(this.elements.display.seekTooltip, visible, show);
const toggle = toggle => {
toggleClass(this.elements.display.seekTooltip, visible, toggle);
};
// Hide on touch // Hide on touch
if (this.touch) { if (this.touch) {
@@ -715,6 +707,9 @@ const controls = {
} }
// Determine percentage, if already visible // Determine percentage, if already visible
let percent = 0;
const clientRect = this.elements.progress.getBoundingClientRect();
if (is.event(event)) { if (is.event(event)) {
percent = (100 / clientRect.width) * (event.pageX - clientRect.left); percent = (100 / clientRect.width) * (event.pageX - clientRect.left);
} else if (hasClass(this.elements.display.seekTooltip, visible)) { } else if (hasClass(this.elements.display.seekTooltip, visible)) {
@@ -1047,7 +1042,7 @@ const controls = {
}, },
// Set a list of available captions languages // Set a list of available captions languages
setSpeedMenu(options) { setSpeedMenu() {
// Menu required // Menu required
if (!is.element(this.elements.settings.panels.speed)) { if (!is.element(this.elements.settings.panels.speed)) {
return; return;
@@ -1056,15 +1051,8 @@ const controls = {
const type = 'speed'; const type = 'speed';
const list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); const list = this.elements.settings.panels.speed.querySelector('[role="menu"]');
// Set the speed options // Filter out invalid speeds
if (is.array(options)) { this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed);
this.options.speed = options;
} else if (this.isHTML5 || this.isVimeo) {
this.options.speed = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
}
// Set options if passed and filter based on config
this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed));
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1; const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;
@@ -1111,7 +1099,7 @@ const controls = {
let target = pane; let target = pane;
if (!is.element(target)) { if (!is.element(target)) {
target = Object.values(this.elements.settings.panels).find(pane => !pane.hidden); target = Object.values(this.elements.settings.panels).find(p => !p.hidden);
} }
const firstItem = target.querySelector('[role^="menuitem"]'); const firstItem = target.querySelector('[role^="menuitem"]');
@@ -1138,7 +1126,10 @@ const controls = {
} else if (is.keyboardEvent(input) && input.which === 27) { } else if (is.keyboardEvent(input) && input.which === 27) {
show = false; show = false;
} else if (is.event(input)) { } else if (is.event(input)) {
const isMenuItem = popup.contains(input.target); // If Plyr is in a shadowDOM, the event target is set to the component, instead of the
// Element in the shadowDOM. The path, if available, is complete.
const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;
const isMenuItem = popup.contains(target);
// If the click was inside the menu or if the click // If the click was inside the menu or if the click
// wasn't the button or menu item and we're trying to // wasn't the button or menu item and we're trying to
@@ -1191,7 +1182,7 @@ const controls = {
// Show a panel in the menu // Show a panel in the menu
showMenuPanel(type = '', tabFocus = false) { showMenuPanel(type = '', tabFocus = false) {
const target = document.getElementById(`plyr-settings-${this.id}-${type}`); const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);
// Nothing to show, bail // Nothing to show, bail
if (!is.element(target)) { if (!is.element(target)) {
@@ -1244,8 +1235,8 @@ const controls = {
controls.focusFirstMenuItem.call(this, target, tabFocus); controls.focusFirstMenuItem.call(this, target, tabFocus);
}, },
// Set the download link // Set the download URL
setDownloadLink() { setDownloadUrl() {
const button = this.elements.buttons.download; const button = this.elements.buttons.download;
// Bail if no button // Bail if no button
@@ -1253,49 +1244,75 @@ const controls = {
return; return;
} }
// Set download link // Set attribute
button.setAttribute('href', this.download); button.setAttribute('href', this.download);
}, },
// Build the default HTML // Build the default HTML
// TODO: Set order based on order in the config.controls array?
create(data) { create(data) {
const {
bindMenuItemShortcuts,
createButton,
createProgress,
createRange,
createTime,
setQualityMenu,
setSpeedMenu,
showMenuPanel,
} = controls;
this.elements.controls = null;
// Larger overlaid play button
if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {
this.elements.container.appendChild(createButton.call(this, 'play-large'));
}
// Create the container // Create the container
const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper)); const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));
this.elements.controls = container;
// Default item attributes
const defaultAttributes = { class: 'plyr__controls__item' };
// Loop through controls in order
dedupe(is.array(this.config.controls) ? this.config.controls: []).forEach(control => {
// Restart button // Restart button
if (this.config.controls.includes('restart')) { if (control === 'restart') {
container.appendChild(controls.createButton.call(this, 'restart')); container.appendChild(createButton.call(this, 'restart', defaultAttributes));
} }
// Rewind button // Rewind button
if (this.config.controls.includes('rewind')) { if (control === 'rewind') {
container.appendChild(controls.createButton.call(this, 'rewind')); container.appendChild(createButton.call(this, 'rewind', defaultAttributes));
} }
// Play/Pause button // Play/Pause button
if (this.config.controls.includes('play')) { if (control === 'play') {
container.appendChild(controls.createButton.call(this, 'play')); container.appendChild(createButton.call(this, 'play', defaultAttributes));
} }
// Fast forward button // Fast forward button
if (this.config.controls.includes('fast-forward')) { if (control === 'fast-forward') {
container.appendChild(controls.createButton.call(this, 'fast-forward')); container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));
} }
// Progress // Progress
if (this.config.controls.includes('progress')) { if (control === 'progress') {
const progressContainer = createElement('div', {
class: `${defaultAttributes.class} plyr__progress__container`,
});
const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress)); const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
// Seek range slider // Seek range slider
progress.appendChild( progress.appendChild(
controls.createRange.call(this, 'seek', { createRange.call(this, 'seek', {
id: `plyr-seek-${data.id}`, id: `plyr-seek-${data.id}`,
}), }),
); );
// Buffer progress // Buffer progress
progress.appendChild(controls.createProgress.call(this, 'buffer')); progress.appendChild(createProgress.call(this, 'buffer'));
// TODO: Add loop display indicator // TODO: Add loop display indicator
@@ -1314,32 +1331,47 @@ const controls = {
} }
this.elements.progress = progress; this.elements.progress = progress;
container.appendChild(this.elements.progress); progressContainer.appendChild(this.elements.progress);
container.appendChild(progressContainer);
} }
// Media current time display // Media current time display
if (this.config.controls.includes('current-time')) { if (control === 'current-time') {
container.appendChild(controls.createTime.call(this, 'currentTime')); container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));
} }
// Media duration display // Media duration display
if (this.config.controls.includes('duration')) { if (control === 'duration') {
container.appendChild(controls.createTime.call(this, 'duration')); container.appendChild(createTime.call(this, 'duration', defaultAttributes));
} }
// Volume controls // Volume controls
if (this.config.controls.includes('mute') || this.config.controls.includes('volume')) { if (control === 'mute' || control === 'volume') {
const volume = createElement('div', { let { volume } = this.elements;
class: 'plyr__volume',
}); // Create the volume container if needed
if (!is.element(volume) || !container.contains(volume)) {
volume = createElement(
'div',
extend({}, defaultAttributes, {
class: `${defaultAttributes.class} plyr__volume`.trim(),
}),
);
this.elements.volume = volume;
container.appendChild(volume);
}
// Toggle mute button // Toggle mute button
if (this.config.controls.includes('mute')) { if (control === 'mute') {
volume.appendChild(controls.createButton.call(this, 'mute')); volume.appendChild(createButton.call(this, 'mute'));
} }
// Volume range control // Volume range control
if (this.config.controls.includes('volume')) { // Ignored on iOS as it's handled globally
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
if (control === 'volume' && !browser.isIos) {
// Set the attributes // Set the attributes
const attributes = { const attributes = {
max: 1, max: 1,
@@ -1349,7 +1381,7 @@ const controls = {
// Create the volume range slider // Create the volume range slider
volume.appendChild( volume.appendChild(
controls.createRange.call( createRange.call(
this, this,
'volume', 'volume',
extend(attributes, { extend(attributes, {
@@ -1357,27 +1389,26 @@ const controls = {
}), }),
), ),
); );
this.elements.volume = volume;
} }
container.appendChild(volume);
} }
// Toggle captions button // Toggle captions button
if (this.config.controls.includes('captions')) { if (control === 'captions') {
container.appendChild(controls.createButton.call(this, 'captions')); container.appendChild(createButton.call(this, 'captions', defaultAttributes));
} }
// Settings button / menu // Settings button / menu
if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) { if (control === 'settings' && !is.empty(this.config.settings)) {
const control = createElement('div', { const wrapper = createElement(
class: 'plyr__menu', 'div',
extend({}, defaultAttributes, {
class: `${defaultAttributes.class} plyr__menu`.trim(),
hidden: '', hidden: '',
}); }),
);
control.appendChild( wrapper.appendChild(
controls.createButton.call(this, 'settings', { createButton.call(this, 'settings', {
'aria-haspopup': true, 'aria-haspopup': true,
'aria-controls': `plyr-settings-${data.id}`, 'aria-controls': `plyr-settings-${data.id}`,
'aria-expanded': false, 'aria-expanded': false,
@@ -1420,11 +1451,11 @@ const controls = {
); );
// Bind menu shortcuts for keyboard users // Bind menu shortcuts for keyboard users
controls.bindMenuItemShortcuts.call(this, menuItem, type); bindMenuItemShortcuts.call(this, menuItem, type);
// Show menu on click // Show menu on click
on(menuItem, 'click', () => { on.call(this, menuItem, 'click', () => {
controls.showMenuPanel.call(this, type, false); showMenuPanel.call(this, type, false);
}); });
const flex = createElement('span', null, i18n.get(type, this.config)); const flex = createElement('span', null, i18n.get(type, this.config));
@@ -1475,7 +1506,8 @@ const controls = {
); );
// Go back via keyboard // Go back via keyboard
on( on.call(
this,
pane, pane,
'keydown', 'keydown',
event => { event => {
@@ -1489,14 +1521,14 @@ const controls = {
event.stopPropagation(); event.stopPropagation();
// Show the respective menu // Show the respective menu
controls.showMenuPanel.call(this, 'home', true); showMenuPanel.call(this, 'home', true);
}, },
false, false,
); );
// Go back via button click // Go back via button click
on(backButton, 'click', () => { on.call(this, backButton, 'click', () => {
controls.showMenuPanel.call(this, 'home', false); showMenuPanel.call(this, 'home', false);
}); });
// Add to pane // Add to pane
@@ -1516,30 +1548,35 @@ const controls = {
}); });
popup.appendChild(inner); popup.appendChild(inner);
control.appendChild(popup); wrapper.appendChild(popup);
container.appendChild(control); container.appendChild(wrapper);
this.elements.settings.popup = popup; this.elements.settings.popup = popup;
this.elements.settings.menu = control; this.elements.settings.menu = wrapper;
} }
// Picture in picture button // Picture in picture button
if (this.config.controls.includes('pip') && support.pip) { if (control === 'pip' && support.pip) {
container.appendChild(controls.createButton.call(this, 'pip')); container.appendChild(createButton.call(this, 'pip', defaultAttributes));
} }
// Airplay button // Airplay button
if (this.config.controls.includes('airplay') && support.airplay) { if (control === 'airplay' && support.airplay) {
container.appendChild(controls.createButton.call(this, 'airplay')); container.appendChild(createButton.call(this, 'airplay', defaultAttributes));
} }
// Download button // Download button
if (this.config.controls.includes('download')) { if (control === 'download') {
const attributes = { const attributes = extend({}, defaultAttributes, {
element: 'a', element: 'a',
href: this.download, href: this.download,
target: '_blank', target: '_blank',
}; });
// Set download attribute for HTML5 only
if (this.isHTML5) {
attributes.download = '';
}
const { download } = this.config.urls; const { download } = this.config.urls;
@@ -1550,27 +1587,21 @@ const controls = {
}); });
} }
container.appendChild(controls.createButton.call(this, 'download', attributes)); container.appendChild(createButton.call(this, 'download', attributes));
} }
// Toggle fullscreen button // Toggle fullscreen button
if (this.config.controls.includes('fullscreen')) { if (control === 'fullscreen') {
container.appendChild(controls.createButton.call(this, 'fullscreen')); container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));
} }
});
// Larger overlaid play button
if (this.config.controls.includes('play-large')) {
this.elements.container.appendChild(controls.createButton.call(this, 'play-large'));
}
this.elements.controls = container;
// Set available quality levels // Set available quality levels
if (this.isHTML5) { if (this.isHTML5) {
controls.setQualityMenu.call(this, html5.getQualityOptions.call(this)); setQualityMenu.call(this, html5.getQualityOptions.call(this));
} }
controls.setSpeedMenu.call(this); setSpeedMenu.call(this);
return container; return container;
}, },
@@ -1644,8 +1675,6 @@ const controls = {
if (update) { if (update) {
if (is.string(this.config.controls)) { if (is.string(this.config.controls)) {
container = replace(container); container = replace(container);
} else if (is.element(container)) {
container.innerHTML = replace(container.innerHTML);
} }
} }
+105 -81
View File
@@ -4,83 +4,11 @@
// https://webkit.org/blog/7929/designing-websites-for-iphone-x/ // https://webkit.org/blog/7929/designing-websites-for-iphone-x/
// ========================================================================== // ==========================================================================
import { repaint } from './utils/animation';
import browser from './utils/browser'; import browser from './utils/browser';
import { hasClass, toggleClass, trapFocus } from './utils/elements'; import { closest,getElements, hasClass, toggleClass } from './utils/elements';
import { on, triggerEvent } from './utils/events'; import { on, triggerEvent } from './utils/events';
import is from './utils/is'; import is from './utils/is';
import { silencePromise } from './utils/promise';
function onChange() {
if (!this.enabled) {
return;
}
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (is.element(button)) {
button.pressed = this.active;
}
// Trigger an event
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
// Trap focus in container
if (!browser.isIos) {
trapFocus.call(this.player, this.target, this.active);
}
}
function toggleFallback(toggle = false) {
// Store or restore scroll position
if (toggle) {
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
}
// Toggle scroll
document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Force full viewport on iPhone X+
if (browser.isIos) {
let viewport = document.head.querySelector('meta[name="viewport"]');
const property = 'viewport-fit=cover';
// Inject the viewport meta if required
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
}
// Check if the property already exists
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) {
viewport.content += `,${property}`;
}
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
.filter(part => part.trim() !== property)
.join(',');
}
// Force a repaint as sometimes Safari doesn't want to fill the screen
setTimeout(() => repaint(this.target), 100);
}
// Toggle button and fire events
onChange.call(this);
}
class Fullscreen { class Fullscreen {
constructor(player) { constructor(player) {
@@ -97,6 +25,11 @@ class Fullscreen {
// Force the use of 'full window/browser' rather than fullscreen // Force the use of 'full window/browser' rather than fullscreen
this.forceFallback = player.config.fullscreen.fallback === 'force'; this.forceFallback = player.config.fullscreen.fallback === 'force';
// Get the fullscreen element
// Checks container is an ancestor, defaults to null
this.player.elements.fullscreen =
player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container);
// Register event listeners // Register event listeners
// Handle event (incase user presses escape etc) // Handle event (incase user presses escape etc)
on.call( on.call(
@@ -105,7 +38,7 @@ class Fullscreen {
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => { () => {
// TODO: Filter for target?? // TODO: Filter for target??
onChange.call(this); this.onChange();
}, },
); );
@@ -119,6 +52,9 @@ class Fullscreen {
this.toggle(); this.toggle();
}); });
// Tap focus when in fullscreen
on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));
// Update the UI // Update the UI
this.update(); this.update();
} }
@@ -188,14 +124,102 @@ class Fullscreen {
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`]; const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
return element === this.target; return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;
} }
// Get target element // Get target element
get target() { get target() {
return browser.isIos && this.player.config.fullscreen.iosNative return browser.isIos && this.player.config.fullscreen.iosNative
? this.player.media ? this.player.media
: this.player.elements.container; : this.player.elements.fullscreen || this.player.elements.container;
}
onChange() {
if (!this.enabled) {
return;
}
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (is.element(button)) {
button.pressed = this.active;
}
// Trigger an event
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
}
toggleFallback(toggle = false) {
// Store or restore scroll position
if (toggle) {
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
}
// Toggle scroll
document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Force full viewport on iPhone X+
if (browser.isIos) {
let viewport = document.head.querySelector('meta[name="viewport"]');
const property = 'viewport-fit=cover';
// Inject the viewport meta if required
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
}
// Check if the property already exists
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) {
viewport.content += `,${property}`;
}
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
.filter(part => part.trim() !== property)
.join(',');
}
}
// Toggle button and fire events
this.onChange();
}
// Trap focus inside container
trapFocus(event) {
// Bail if iOS, not active, not the tab key
if (browser.isIos || !this.active || event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element
const focused = document.activeElement;
const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');
const [first] = focusable;
const last = focusable[focusable.length - 1];
if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used
first.focus();
event.preventDefault();
} else if (focused === first && event.shiftKey) {
// Move focus to last element that can be tabbed if Shift is used
last.focus();
event.preventDefault();
}
} }
// Update UI // Update UI
@@ -230,9 +254,9 @@ class Fullscreen {
if (browser.isIos && this.player.config.fullscreen.iosNative) { if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitEnterFullscreen(); this.target.webkitEnterFullscreen();
} else if (!Fullscreen.native || this.forceFallback) { } else if (!Fullscreen.native || this.forceFallback) {
toggleFallback.call(this, true); this.toggleFallback(true);
} else if (!this.prefix) { } else if (!this.prefix) {
this.target.requestFullscreen(); this.target.requestFullscreen({ navigationUI: 'hide' });
} else if (!is.empty(this.prefix)) { } else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`](); this.target[`${this.prefix}Request${this.property}`]();
} }
@@ -247,9 +271,9 @@ class Fullscreen {
// iOS native fullscreen // iOS native fullscreen
if (browser.isIos && this.player.config.fullscreen.iosNative) { if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen(); this.target.webkitExitFullscreen();
this.player.play(); silencePromise(this.player.play());
} else if (!Fullscreen.native || this.forceFallback) { } else if (!Fullscreen.native || this.forceFallback) {
toggleFallback.call(this, false); this.toggleFallback(false);
} else if (!this.prefix) { } else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document); (document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!is.empty(this.prefix)) { } else if (!is.empty(this.prefix)) {
+30 -6
View File
@@ -6,6 +6,8 @@ import support from './support';
import { removeElement } from './utils/elements'; import { removeElement } from './utils/elements';
import { triggerEvent } from './utils/events'; import { triggerEvent } from './utils/events';
import is from './utils/is'; import is from './utils/is';
import { silencePromise } from './utils/promise';
import { setAspectRatio } from './utils/style';
const html5 = { const html5 = {
getSources() { getSources() {
@@ -29,6 +31,11 @@ const html5 = {
// Get quality levels // Get quality levels
getQualityOptions() { getQualityOptions() {
// Whether we're forcing all options (e.g. for streaming)
if (this.config.quality.forced) {
return this.config.quality.options;
}
// Get sizes from <source> elements // Get sizes from <source> elements
return html5.getSources return html5.getSources
.call(this) .call(this)
@@ -36,29 +43,44 @@ const html5 = {
.filter(Boolean); .filter(Boolean);
}, },
extend() { setup() {
if (!this.isHTML5) { if (!this.isHTML5) {
return; return;
} }
const player = this; const player = this;
// Set speed options from config
player.options.speed = player.config.speed.options;
// Set aspect ratio if fixed
if (!is.empty(this.config.ratio)) {
setAspectRatio.call(player);
}
// Quality // Quality
Object.defineProperty(player.media, 'quality', { Object.defineProperty(player.media, 'quality', {
get() { get() {
// Get sources // Get sources
const sources = html5.getSources.call(player); const sources = html5.getSources.call(player);
const source = sources.find(source => source.getAttribute('src') === player.source); const source = sources.find(s => s.getAttribute('src') === player.source);
// Return size, if match is found // Return size, if match is found
return source && Number(source.getAttribute('size')); return source && Number(source.getAttribute('size'));
}, },
set(input) { set(input) {
if (player.quality === input) {
return;
}
// If we're using an an external handler...
if (player.config.quality.forced && is.function(player.config.quality.onChange)) {
player.config.quality.onChange(input);
} else {
// Get sources // Get sources
const sources = html5.getSources.call(player); const sources = html5.getSources.call(player);
// Get first match for requested size // Get first match for requested size
const source = sources.find(source => Number(source.getAttribute('size')) === input); const source = sources.find(s => Number(s.getAttribute('size')) === input);
// No matching source found // No matching source found
if (!source) { if (!source) {
@@ -66,7 +88,7 @@ const html5 = {
} }
// Get current state // Get current state
const { currentTime, paused, preload, readyState } = player.media; const { currentTime, paused, preload, readyState, playbackRate } = player.media;
// Set new source // Set new source
player.media.src = source.getAttribute('src'); player.media.src = source.getAttribute('src');
@@ -75,17 +97,19 @@ const html5 = {
if (preload !== 'none' || readyState) { if (preload !== 'none' || readyState) {
// Restore time // Restore time
player.once('loadedmetadata', () => { player.once('loadedmetadata', () => {
player.speed = playbackRate;
player.currentTime = currentTime; player.currentTime = currentTime;
// Resume playing // Resume playing
if (!paused) { if (!paused) {
player.play(); silencePromise(player.play());
} }
}); });
// Load new source // Load new source
player.media.load(); player.media.load();
} }
}
// Trigger change event // Trigger change event
triggerEvent.call(player, player.media, 'qualitychange', false, { triggerEvent.call(player, player.media, 'qualitychange', false, {
+76 -50
View File
@@ -6,10 +6,11 @@ import controls from './controls';
import ui from './ui'; import ui from './ui';
import { repaint } from './utils/animation'; import { repaint } from './utils/animation';
import browser from './utils/browser'; import browser from './utils/browser';
import { getElement, getElements, matches, toggleClass, toggleHidden } from './utils/elements'; import { getElement, getElements, matches, toggleClass } from './utils/elements';
import { off, on, once, toggleListener, triggerEvent } from './utils/events'; import { off, on, once, toggleListener, triggerEvent } from './utils/events';
import is from './utils/is'; import is from './utils/is';
import { setAspectRatio } from './utils/style'; import { silencePromise } from './utils/promise';
import { getAspectRatio, setAspectRatio } from './utils/style';
class Listeners { class Listeners {
constructor(player) { constructor(player) {
@@ -99,7 +100,7 @@ class Listeners {
case 75: case 75:
// Space and K key // Space and K key
if (!repeat) { if (!repeat) {
player.togglePlay(); silencePromise(player.togglePlay());
} }
break; break;
@@ -228,6 +229,7 @@ class Listeners {
// Delay the adding of classname until the focus has changed // Delay the adding of classname until the focus has changed
// This event fires before the focusin event // This event fires before the focusin event
if (event.type !== 'focusout') {
this.focusTimer = setTimeout(() => { this.focusTimer = setTimeout(() => {
const focused = document.activeElement; const focused = document.activeElement;
@@ -239,6 +241,7 @@ class Listeners {
toggleClass(document.activeElement, player.config.classNames.tabFocus, true); toggleClass(document.activeElement, player.config.classNames.tabFocus, true);
}, 10); }, 10);
} }
}
// Global window & document listeners // Global window & document listeners
global(toggle = true) { global(toggle = true) {
@@ -256,7 +259,7 @@ class Listeners {
once.call(player, document.body, 'touchstart', this.firstTouch); once.call(player, document.body, 'touchstart', this.firstTouch);
// Tab focus detection // Tab focus detection
toggleListener.call(player, document.body, 'keydown focus blur', this.setTabFocus, toggle, false, true); toggleListener.call(player, document.body, 'keydown focus blur focusout', this.setTabFocus, toggle, false, true);
} }
// Container listeners // Container listeners
@@ -275,17 +278,16 @@ class Listeners {
elements.container, elements.container,
'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',
event => { event => {
const { controls } = elements; const { controls: controlsElement } = elements;
// Remove button states for fullscreen // Remove button states for fullscreen
if (controls && event.type === 'enterfullscreen') { if (controlsElement && event.type === 'enterfullscreen') {
controls.pressed = false; controlsElement.pressed = false;
controls.hover = false; controlsElement.hover = false;
} }
// Show, then hide after a timeout unless another control event occurs // Show, then hide after a timeout unless another control event occurs
const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type); const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
let delay = 0; let delay = 0;
if (show) { if (show) {
@@ -302,25 +304,17 @@ class Listeners {
}, },
); );
// Force edge to repaint on exit fullscreen
// TODO: Fix weird bug where Edge doesn't re-draw when exiting fullscreen
/* if (browser.isEdge) {
on.call(player, elements.container, 'exitfullscreen', () => {
setTimeout(() => repaint(elements.container), 100);
});
} */
// Set a gutter for Vimeo // Set a gutter for Vimeo
const setGutter = (ratio, padding, toggle) => { const setGutter = (ratio, padding, toggle) => {
if (!player.isVimeo) { if (!player.isVimeo || player.config.vimeo.premium) {
return; return;
} }
const target = player.elements.wrapper.firstChild; const target = player.elements.wrapper.firstChild;
const [, height] = ratio.split(':').map(Number); const [, y] = ratio;
const [videoWidth, videoHeight] = player.embed.ratio.split(':').map(Number); const [videoX, videoY] = getAspectRatio.call(player);
target.style.maxWidth = toggle ? `${(height / videoHeight) * videoWidth}px` : null; target.style.maxWidth = toggle ? `${(y / videoY) * videoX}px` : null;
target.style.margin = toggle ? '0 auto' : null; target.style.margin = toggle ? '0 auto' : null;
}; };
@@ -338,27 +332,31 @@ class Listeners {
}; };
const resized = () => { const resized = () => {
window.clearTimeout(timers.resized); clearTimeout(timers.resized);
timers.resized = window.setTimeout(setPlayerSize, 50); timers.resized = setTimeout(setPlayerSize, 50);
}; };
on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => { on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {
const { target, usingNative } = player.fullscreen; const { target, usingNative } = player.fullscreen;
// Ignore for iOS native // Ignore events not from target
if (!player.isEmbed || target !== elements.container) { if (target !== elements.container) {
return;
}
// If it's not an embed and no ratio specified
if (!player.isEmbed && is.empty(player.config.ratio)) {
return; return;
} }
const isEnter = event.type === 'enterfullscreen'; const isEnter = event.type === 'enterfullscreen';
// Set the player size when entering fullscreen to viewport size // Set the player size when entering fullscreen to viewport size
const { padding, ratio } = setPlayerSize(isEnter); const { padding, ratio } = setPlayerSize(isEnter);
// Set Vimeo gutter // Set Vimeo gutter
setGutter(ratio, padding, isEnter); setGutter(ratio, padding, isEnter);
// If not using native fullscreen, we need to check for resizes of viewport // If not using native browser fullscreen API, we need to check for resizes of viewport
if (!usingNative) { if (!usingNative) {
if (isEnter) { if (isEnter) {
on.call(player, window, 'resize', resized); on.call(player, window, 'resize', resized);
@@ -382,19 +380,15 @@ class Listeners {
controls.durationUpdate.call(player, event), controls.durationUpdate.call(player, event),
); );
// Check for audio tracks on load
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
on.call(player, player.media, 'canplay loadeddata', () => {
toggleHidden(elements.volume, !player.hasAudio);
toggleHidden(elements.buttons.mute, !player.hasAudio);
});
// Handle the media finishing // Handle the media finishing
on.call(player, player.media, 'ended', () => { on.call(player, player.media, 'ended', () => {
// Show poster on end // Show poster on end
if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) { if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {
// Restart // Restart
player.restart(); player.restart();
// Call pause otherwise IE11 will start playing the video again
player.pause();
} }
}); });
@@ -440,9 +434,21 @@ class Listeners {
if (player.ended) { if (player.ended) {
this.proxy(event, player.restart, 'restart'); this.proxy(event, player.restart, 'restart');
this.proxy(event, player.play, 'play'); this.proxy(
event,
() => {
silencePromise(player.play());
},
'play',
);
} else { } else {
this.proxy(event, player.togglePlay, 'play'); this.proxy(
event,
() => {
silencePromise(player.togglePlay());
},
'play',
);
} }
}); });
} }
@@ -486,7 +492,7 @@ class Listeners {
// Update download link when ready and if quality changes // Update download link when ready and if quality changes
on.call(player, player.media, 'ready qualitychange', () => { on.call(player, player.media, 'ready qualitychange', () => {
controls.setDownloadLink.call(player); controls.setDownloadUrl.call(player);
}); });
// Proxy events to container // Proxy events to container
@@ -518,7 +524,7 @@ class Listeners {
} }
// Only call default handler if not prevented in custom handler // Only call default handler if not prevented in custom handler
if (returned && is.function(defaultHandler)) { if (returned !== false && is.function(defaultHandler)) {
defaultHandler.call(player, event); defaultHandler.call(player, event);
} }
} }
@@ -542,14 +548,20 @@ class Listeners {
controls() { controls() {
const { player } = this; const { player } = this;
const { elements } = player; const { elements } = player;
// IE doesn't support input event, so we fallback to change // IE doesn't support input event, so we fallback to change
const inputEvent = browser.isIE ? 'change' : 'input'; const inputEvent = browser.isIE ? 'change' : 'input';
// Play/pause toggle // Play/pause toggle
if (elements.buttons.play) { if (elements.buttons.play) {
Array.from(elements.buttons.play).forEach(button => { Array.from(elements.buttons.play).forEach(button => {
this.bind(button, 'click', player.togglePlay, 'play'); this.bind(
button,
'click',
() => {
silencePromise(player.togglePlay());
},
'play',
);
}); });
} }
@@ -609,12 +621,19 @@ class Listeners {
this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay'); this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');
// Settings menu - click toggle // Settings menu - click toggle
this.bind(elements.buttons.settings, 'click', event => { this.bind(
elements.buttons.settings,
'click',
event => {
// Prevent the document click listener closing the menu // Prevent the document click listener closing the menu
event.stopPropagation(); event.stopPropagation();
event.preventDefault();
controls.toggleMenu.call(player, event); controls.toggleMenu.call(player, event);
}); },
null,
false,
); // Can't be passive as we're preventing default
// Settings menu - keyboard toggle // Settings menu - keyboard toggle
// We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus
@@ -669,7 +688,7 @@ class Listeners {
const code = event.keyCode ? event.keyCode : event.which; const code = event.keyCode ? event.keyCode : event.which;
const attribute = 'play-on-seeked'; const attribute = 'play-on-seeked';
if (is.keyboardEvent(event) && (code !== 39 && code !== 37)) { if (is.keyboardEvent(event) && code !== 39 && code !== 37) {
return; return;
} }
@@ -678,14 +697,13 @@ class Listeners {
// Was playing before? // Was playing before?
const play = seek.hasAttribute(attribute); const play = seek.hasAttribute(attribute);
// Done seeking // Done seeking
const done = ['mouseup', 'touchend', 'keyup'].includes(event.type); const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
// If we're done seeking and it was playing, resume playback // If we're done seeking and it was playing, resume playback
if (play && done) { if (play && done) {
seek.removeAttribute(attribute); seek.removeAttribute(attribute);
player.play(); silencePromise(player.play());
} else if (!done && player.playing) { } else if (!done && player.playing) {
seek.setAttribute(attribute, ''); seek.setAttribute(attribute, '');
player.pause(); player.pause();
@@ -706,7 +724,6 @@ class Listeners {
inputEvent, inputEvent,
event => { event => {
const seek = event.currentTarget; const seek = event.currentTarget;
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954) // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
let seekTo = seek.getAttribute('seek-value'); let seekTo = seek.getAttribute('seek-value');
@@ -737,7 +754,7 @@ class Listeners {
}); });
// Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering
this.bind(elements.progress, 'mouseleave click', () => { this.bind(elements.progress, 'mouseleave touchend click', () => {
const { previewThumbnails } = player; const { previewThumbnails } = player;
if (previewThumbnails && previewThumbnails.loaded) { if (previewThumbnails && previewThumbnails.loaded) {
@@ -799,6 +816,17 @@ class Listeners {
elements.controls.hover = !player.touch && event.type === 'mouseenter'; elements.controls.hover = !player.touch && event.type === 'mouseenter';
}); });
// Also update controls.hover state for any non-player children of fullscreen element (as above)
if (elements.fullscreen) {
Array.from(elements.fullscreen.children)
.filter(c => !c.contains(elements.container))
.forEach(child => {
this.bind(child, 'mouseenter mouseleave', event => {
elements.controls.hover = !player.touch && event.type === 'mouseenter';
});
});
}
// Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
@@ -806,7 +834,7 @@ class Listeners {
// Show controls when they receive focus (e.g., when using keyboard tab key) // Show controls when they receive focus (e.g., when using keyboard tab key)
this.bind(elements.controls, 'focusin', () => { this.bind(elements.controls, 'focusin', () => {
const { config, elements, timers } = player; const { config, timers } = player;
// Skip transition to prevent focus from scrolling the parent element // Skip transition to prevent focus from scrolling the parent element
toggleClass(elements.controls, config.classNames.noTransition, true); toggleClass(elements.controls, config.classNames.noTransition, true);
@@ -837,10 +865,8 @@ class Listeners {
// Detect "natural" scroll - suppored on OS X Safari only // Detect "natural" scroll - suppored on OS X Safari only
// Other browsers on OS X will be inverted until support improves // Other browsers on OS X will be inverted until support improves
const inverted = event.webkitDirectionInvertedFromDevice; const inverted = event.webkitDirectionInvertedFromDevice;
// Get delta from event. Invert if `inverted` is true // Get delta from event. Invert if `inverted` is true
const [x, y] = [event.deltaX, -event.deltaY].map(value => (inverted ? -value : value)); const [x, y] = [event.deltaX, -event.deltaY].map(value => (inverted ? -value : value));
// Using the biggest delta, normalize to 1 or -1 (or 0 if no delta) // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)
const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);
+2 -2
View File
@@ -38,7 +38,7 @@ const media = {
// Wrap the video in a container // Wrap the video in a container
wrap(this.media, this.elements.wrapper); wrap(this.media, this.elements.wrapper);
// Faux poster container // Poster image container
this.elements.poster = createElement('div', { this.elements.poster = createElement('div', {
class: this.config.classNames.poster, class: this.config.classNames.poster,
}); });
@@ -47,7 +47,7 @@ const media = {
} }
if (this.isHTML5) { if (this.isHTML5) {
html5.extend.call(this); html5.setup.call(this);
} else if (this.isYouTube) { } else if (this.isYouTube) {
youtube.setup.call(this); youtube.setup.call(this);
} else if (this.isVimeo) { } else if (this.isVimeo) {
+62 -41
View File
@@ -10,10 +10,25 @@ import { createElement } from '../utils/elements';
import { triggerEvent } from '../utils/events'; import { triggerEvent } from '../utils/events';
import i18n from '../utils/i18n'; import i18n from '../utils/i18n';
import is from '../utils/is'; import is from '../utils/is';
import loadScript from '../utils/loadScript'; import loadScript from '../utils/load-script';
import { silencePromise } from '../utils/promise';
import { formatTime } from '../utils/time'; import { formatTime } from '../utils/time';
import { buildUrlParams } from '../utils/urls'; import { buildUrlParams } from '../utils/urls';
const destroy = instance => {
// Destroy our adsManager
if (instance.manager) {
instance.manager.destroy();
}
// Destroy our adsManager
if (instance.elements.displayContainer) {
instance.elements.displayContainer.destroy();
}
instance.elements.container.remove();
};
class Ads { class Ads {
/** /**
* Ads constructor. * Ads constructor.
@@ -63,7 +78,10 @@ class Ads {
* Load the IMA SDK * Load the IMA SDK
*/ */
load() { load() {
if (this.enabled) { if (!this.enabled) {
return;
}
// Check if the Google IMA3 SDK is loaded or load it ourselves // Check if the Google IMA3 SDK is loaded or load it ourselves
if (!is.object(window.google) || !is.object(window.google.ima)) { if (!is.object(window.google) || !is.object(window.google.ima)) {
loadScript(this.player.config.urls.googleIMA.sdk) loadScript(this.player.config.urls.googleIMA.sdk)
@@ -78,12 +96,16 @@ class Ads {
this.ready(); this.ready();
} }
} }
}
/** /**
* Get the ads instance ready * Get the ads instance ready
*/ */
ready() { ready() {
// Double check we're enabled
if (!this.enabled) {
destroy(this);
}
// Start ticking our safety timer. If the whole advertisement // Start ticking our safety timer. If the whole advertisement
// thing doesn't resolve within our set time; we bail // thing doesn't resolve within our set time; we bail
this.startSafetyTimer(12000, 'ready()'); this.startSafetyTimer(12000, 'ready()');
@@ -115,7 +137,7 @@ class Ads {
cb: Date.now(), cb: Date.now(),
AV_WIDTH: 640, AV_WIDTH: 640,
AV_HEIGHT: 480, AV_HEIGHT: 480,
AV_CDIM2: this.publisherId, AV_CDIM2: config.publisherId,
}; };
const base = 'https://go.aniview.com/api/adserver6/vast/'; const base = 'https://go.aniview.com/api/adserver6/vast/';
@@ -151,17 +173,6 @@ class Ads {
// We assume the adContainer is the video container of the plyr element that will house the ads // We assume the adContainer is the video container of the plyr element that will house the ads
this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media); this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);
// Request video ads to be pre-loaded
this.requestAds();
}
/**
* Request advertisements
*/
requestAds() {
const { container } = this.player.elements;
try {
// Create ads loader // Create ads loader
this.loader = new google.ima.AdsLoader(this.elements.displayContainer); this.loader = new google.ima.AdsLoader(this.elements.displayContainer);
@@ -173,6 +184,17 @@ class Ads {
); );
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false); this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);
// Request video ads to be pre-loaded
this.requestAds();
}
/**
* Request advertisements
*/
requestAds() {
const { container } = this.player.elements;
try {
// Request video ads // Request video ads
const request = new google.ima.AdsRequest(); const request = new google.ima.AdsRequest();
request.adTagUrl = this.tagUrl; request.adTagUrl = this.tagUrl;
@@ -240,16 +262,13 @@ class Ads {
// Get the cue points for any mid-rolls by filtering out the pre- and post-roll // Get the cue points for any mid-rolls by filtering out the pre- and post-roll
this.cuePoints = this.manager.getCuePoints(); this.cuePoints = this.manager.getCuePoints();
// Set volume to match player
this.manager.setVolume(this.player.volume);
// Add listeners to the required events // Add listeners to the required events
// Advertisement error events // Advertisement error events
this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error)); this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));
// Advertisement regular events // Advertisement regular events
Object.keys(google.ima.AdEvent.Type).forEach(type => { Object.keys(google.ima.AdEvent.Type).forEach(type => {
this.manager.addEventListener(google.ima.AdEvent.Type[type], event => this.onAdEvent(event)); this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e));
}); });
// Resolve our adsManager // Resolve our adsManager
@@ -285,7 +304,6 @@ class Ads {
*/ */
onAdEvent(event) { onAdEvent(event) {
const { container } = this.player.elements; const { container } = this.player.elements;
// Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED) // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)
// don't have ad object associated // don't have ad object associated
const ad = event.getAd(); const ad = event.getAd();
@@ -293,19 +311,18 @@ class Ads {
// Proxy event // Proxy event
const dispatchEvent = type => { const dispatchEvent = type => {
const event = `ads${type.replace(/_/g, '').toLowerCase()}`; triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);
triggerEvent.call(this.player, this.player.media, event);
}; };
// Bubble the event
dispatchEvent(event.type);
switch (event.type) { switch (event.type) {
case google.ima.AdEvent.Type.LOADED: case google.ima.AdEvent.Type.LOADED:
// This is the first event sent for an ad - it is possible to determine whether the // This is the first event sent for an ad - it is possible to determine whether the
// ad is a video ad or an overlay // ad is a video ad or an overlay
this.trigger('loaded'); this.trigger('loaded');
// Bubble event
dispatchEvent(event.type);
// Start countdown // Start countdown
this.pollCountdown(true); this.pollCountdown(true);
@@ -317,15 +334,19 @@ class Ads {
// console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex()); // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());
// console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset()); // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());
break;
case google.ima.AdEvent.Type.STARTED:
// Set volume to match player
this.manager.setVolume(this.player.volume);
break; break;
case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
// All ads for the current videos are done. We can now request new advertisements // All ads for the current videos are done. We can now request new advertisements
// in case the video is re-played // in case the video is re-played
// Fire event
dispatchEvent(event.type);
// TODO: Example for what happens when a next video in a playlist would be loaded. // TODO: Example for what happens when a next video in a playlist would be loaded.
// So here we load a new video when all ads are done. // So here we load a new video when all ads are done.
// Then we load new ads within a new adsManager. When the video // Then we load new ads within a new adsManager. When the video
@@ -349,7 +370,13 @@ class Ads {
// TODO: So there is still this thing where a video should only be allowed to start // TODO: So there is still this thing where a video should only be allowed to start
// playing when the IMA SDK is ready or has failed // playing when the IMA SDK is ready or has failed
if (this.player.ended) {
this.loadAds(); this.loadAds();
} else {
// The SDK won't allow new ads to be called without receiving a contentComplete()
this.loader.contentComplete();
}
break; break;
case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED: case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
@@ -357,8 +384,6 @@ class Ads {
// for example display a pause button and remaining time. Fired when content should // for example display a pause button and remaining time. Fired when content should
// be paused. This usually happens right before an ad is about to cover the content // be paused. This usually happens right before an ad is about to cover the content
dispatchEvent(event.type);
this.pauseContent(); this.pauseContent();
break; break;
@@ -369,26 +394,17 @@ class Ads {
// Fired when content should be resumed. This usually happens when an ad finishes // Fired when content should be resumed. This usually happens when an ad finishes
// or collapses // or collapses
dispatchEvent(event.type);
this.pollCountdown(); this.pollCountdown();
this.resumeContent(); this.resumeContent();
break; break;
case google.ima.AdEvent.Type.STARTED:
case google.ima.AdEvent.Type.MIDPOINT:
case google.ima.AdEvent.Type.COMPLETE:
case google.ima.AdEvent.Type.IMPRESSION:
case google.ima.AdEvent.Type.CLICK:
dispatchEvent(event.type);
break;
case google.ima.AdEvent.Type.LOG: case google.ima.AdEvent.Type.LOG:
if (adData.adError) { if (adData.adError) {
this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`); this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);
} }
break; break;
default: default:
@@ -463,6 +479,9 @@ class Ads {
// Play the requested advertisement whenever the adsManager is ready // Play the requested advertisement whenever the adsManager is ready
this.managerPromise this.managerPromise
.then(() => { .then(() => {
// Set volume to match player
this.manager.setVolume(this.player.volume);
// Initialize the container. Must be done via a user action on mobile devices // Initialize the container. Must be done via a user action on mobile devices
this.elements.displayContainer.initialize(); this.elements.displayContainer.initialize();
@@ -497,7 +516,7 @@ class Ads {
this.playing = false; this.playing = false;
// Play video // Play video
this.player.media.play(); silencePromise(this.player.media.play());
} }
/** /**
@@ -550,6 +569,8 @@ class Ads {
this.on('loaded', resolve); this.on('loaded', resolve);
this.player.debug.log(this.manager); this.player.debug.log(this.manager);
}); });
// Now that the manager has been destroyed set it to also be un-initialized
this.initialized = false;
// Now request some new advertisements // Now request some new advertisements
this.requestAds(); this.requestAds();
+709
View File
@@ -0,0 +1,709 @@
import { createElement } from '../utils/elements';
import { once } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import { formatTime } from '../utils/time';
// Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg"
const parseVtt = vttDataString => {
const processedList = [];
const frames = vttDataString.split(/\r\n\r\n|\n\n|\r\r/);
frames.forEach(frame => {
const result = {};
const lines = frame.split(/\r\n|\n|\r/);
lines.forEach(line => {
if (!is.number(result.startTime)) {
// The line with start and end times on it is the first line of interest
const matchTimes = line.match(
/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,
); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT
if (matchTimes) {
result.startTime =
Number(matchTimes[1] || 0) * 60 * 60 +
Number(matchTimes[2]) * 60 +
Number(matchTimes[3]) +
Number(`0.${matchTimes[4]}`);
result.endTime =
Number(matchTimes[6] || 0) * 60 * 60 +
Number(matchTimes[7]) * 60 +
Number(matchTimes[8]) +
Number(`0.${matchTimes[9]}`);
}
} else if (!is.empty(line.trim()) && is.empty(result.text)) {
// If we already have the startTime, then we're definitely up to the text line(s)
const lineSplit = line.trim().split('#xywh=');
[result.text] = lineSplit;
// If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image
if (lineSplit[1]) {
[result.x, result.y, result.w, result.h] = lineSplit[1].split(',');
}
}
});
if (result.text) {
processedList.push(result);
}
});
return processedList;
};
/**
* Preview thumbnails for seek hover and scrubbing
* Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar
* Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed
*
* Notes:
* - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole
* - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails
* - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered
*/
const fitRatio = (ratio, outer) => {
const targetRatio = outer.width / outer.height;
const result = {};
if (ratio > targetRatio) {
result.width = outer.width;
result.height = (1 / ratio) * outer.width;
} else {
result.height = outer.height;
result.width = ratio * outer.height;
}
return result;
};
class PreviewThumbnails {
/**
* PreviewThumbnails constructor.
* @param {Plyr} player
* @return {PreviewThumbnails}
*/
constructor(player) {
this.player = player;
this.thumbnails = [];
this.loaded = false;
this.lastMouseMoveTime = Date.now();
this.mouseDown = false;
this.loadedImages = [];
this.elements = {
thumb: {},
scrubbing: {},
};
this.load();
}
get enabled() {
return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;
}
load() {
// Toggle the regular seek tooltip
if (this.player.elements.display.seekTooltip) {
this.player.elements.display.seekTooltip.hidden = this.enabled;
}
if (!this.enabled) {
return;
}
this.getThumbnails().then(() => {
if (!this.enabled) {
return;
}
// Render DOM elements
this.render();
// Check to see if thumb container size was specified manually in CSS
this.determineContainerAutoSizing();
this.loaded = true;
});
}
// Download VTT files and parse them
getThumbnails() {
return new Promise(resolve => {
const { src } = this.player.config.previewThumbnails;
if (is.empty(src)) {
throw new Error('Missing previewThumbnails.src config attribute');
}
// Resolve promise
const sortAndResolve = () => {
// Sort smallest to biggest (e.g., [120p, 480p, 1080p])
this.thumbnails.sort((x, y) => x.height - y.height);
this.player.debug.log('Preview thumbnails', this.thumbnails);
resolve();
};
// Via callback()
if (is.function(src)) {
src(thumbnails => {
this.thumbnails = thumbnails;
sortAndResolve();
});
}
// VTT urls
else {
// If string, convert into single-element list
const urls = is.string(src) ? [src] : src;
// Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
const promises = urls.map(u => this.getThumbnail(u));
// Resolve
Promise.all(promises).then(sortAndResolve);
}
});
}
// Process individual VTT file
getThumbnail(url) {
return new Promise(resolve => {
fetch(url).then(response => {
const thumbnail = {
frames: parseVtt(response),
height: null,
urlPrefix: '',
};
// If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file
// If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank
// If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file
if (
!thumbnail.frames[0].text.startsWith('/') &&
!thumbnail.frames[0].text.startsWith('http://') &&
!thumbnail.frames[0].text.startsWith('https://')
) {
thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
}
// Download the first frame, so that we can determine/set the height of this thumbnailsDef
const tempImage = new Image();
tempImage.onload = () => {
thumbnail.height = tempImage.naturalHeight;
thumbnail.width = tempImage.naturalWidth;
this.thumbnails.push(thumbnail);
resolve();
};
tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;
});
});
}
startMove(event) {
if (!this.loaded) {
return;
}
if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) {
return;
}
// Wait until media has a duration
if (!this.player.media.duration) {
return;
}
if (event.type === 'touchmove') {
// Calculate seek hover position as approx video seconds
this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);
} else {
// Calculate seek hover position as approx video seconds
const clientRect = this.player.elements.progress.getBoundingClientRect();
const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left);
this.seekTime = this.player.media.duration * (percentage / 100);
if (this.seekTime < 0) {
// The mousemove fires for 10+px out to the left
this.seekTime = 0;
}
if (this.seekTime > this.player.media.duration - 1) {
// Took 1 second off the duration for safety, because different players can disagree on the real duration of a video
this.seekTime = this.player.media.duration - 1;
}
this.mousePosX = event.pageX;
// Set time text inside image container
this.elements.thumb.time.innerText = formatTime(this.seekTime);
}
// Download and show image
this.showImageAtCurrentTime();
}
endMove() {
this.toggleThumbContainer(false, true);
}
startScrubbing(event) {
// Only act on left mouse button (0), or touch device (event.button does not exist or is false)
if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {
this.mouseDown = true;
// Wait until media has a duration
if (this.player.media.duration) {
this.toggleScrubbingContainer(true);
this.toggleThumbContainer(false, true);
// Download and show image
this.showImageAtCurrentTime();
}
}
}
endScrubbing() {
this.mouseDown = false;
// Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview
if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {
// The video was already seeked/loaded at the chosen time - hide immediately
this.toggleScrubbingContainer(false);
} else {
// The video hasn't seeked yet. Wait for that
once.call(this.player, this.player.media, 'timeupdate', () => {
// Re-check mousedown - we might have already started scrubbing again
if (!this.mouseDown) {
this.toggleScrubbingContainer(false);
}
});
}
}
/**
* Setup hooks for Plyr and window events
*/
listeners() {
// Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering
this.player.on('play', () => {
this.toggleThumbContainer(false, true);
});
this.player.on('seeked', () => {
this.toggleThumbContainer(false);
});
this.player.on('timeupdate', () => {
this.lastTime = this.player.media.currentTime;
});
}
/**
* Create HTML elements for image containers
*/
render() {
// Create HTML element: plyr__preview-thumbnail-container
this.elements.thumb.container = createElement('div', {
class: this.player.config.classNames.previewThumbnails.thumbContainer,
});
// Wrapper for the image for styling
this.elements.thumb.imageContainer = createElement('div', {
class: this.player.config.classNames.previewThumbnails.imageContainer,
});
this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);
// Create HTML element, parent+span: time text (e.g., 01:32:00)
const timeContainer = createElement('div', {
class: this.player.config.classNames.previewThumbnails.timeContainer,
});
this.elements.thumb.time = createElement('span', {}, '00:00');
timeContainer.appendChild(this.elements.thumb.time);
this.elements.thumb.container.appendChild(timeContainer);
// Inject the whole thumb
if (is.element(this.player.elements.progress)) {
this.player.elements.progress.appendChild(this.elements.thumb.container);
}
// Create HTML element: plyr__preview-scrubbing-container
this.elements.scrubbing.container = createElement('div', {
class: this.player.config.classNames.previewThumbnails.scrubbingContainer,
});
this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);
}
destroy() {
if (this.elements.thumb.container) {
this.elements.thumb.container.remove();
}
if (this.elements.scrubbing.container) {
this.elements.scrubbing.container.remove();
}
}
showImageAtCurrentTime() {
if (this.mouseDown) {
this.setScrubbingContainerSize();
} else {
this.setThumbContainerSizeAndPos();
}
// Find the desired thumbnail index
// TODO: Handle a video longer than the thumbs where thumbNum is null
const thumbNum = this.thumbnails[0].frames.findIndex(
frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,
);
const hasThumb = thumbNum >= 0;
let qualityIndex = 0;
// Show the thumb container if we're not scrubbing
if (!this.mouseDown) {
this.toggleThumbContainer(hasThumb);
}
// No matching thumb found
if (!hasThumb) {
return;
}
// Check to see if we've already downloaded higher quality versions of this image
this.thumbnails.forEach((thumbnail, index) => {
if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {
qualityIndex = index;
}
});
// Only proceed if either thumbnum or thumbfilename has changed
if (thumbNum !== this.showingThumb) {
this.showingThumb = thumbNum;
this.loadImage(qualityIndex);
}
}
// Show the image that's currently specified in this.showingThumb
loadImage(qualityIndex = 0) {
const thumbNum = this.showingThumb;
const thumbnail = this.thumbnails[qualityIndex];
const { urlPrefix } = thumbnail;
const frame = thumbnail.frames[thumbNum];
const thumbFilename = thumbnail.frames[thumbNum].text;
const thumbUrl = urlPrefix + thumbFilename;
if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {
// If we're already loading a previous image, remove its onload handler - we don't want it to load after this one
// Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort
if (this.loadingImage && this.usingSprites) {
this.loadingImage.onload = null;
}
// We're building and adding a new image. In other implementations of similar functionality (YouTube), background image
// is instead used. But this causes issues with larger images in Firefox and Safari - switching between background
// images causes a flicker. Putting a new image over the top does not
const previewImage = new Image();
previewImage.src = thumbUrl;
previewImage.dataset.index = thumbNum;
previewImage.dataset.filename = thumbFilename;
this.showingThumbFilename = thumbFilename;
this.player.debug.log(`Loading image: ${thumbUrl}`);
// For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...
previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);
this.loadingImage = previewImage;
this.removeOldImages(previewImage);
} else {
// Update the existing image
this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);
this.currentImageElement.dataset.index = thumbNum;
this.removeOldImages(this.currentImageElement);
}
}
showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) {
this.player.debug.log(
`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`,
);
this.setImageSizeAndOffset(previewImage, frame);
if (newImage) {
this.currentImageContainer.appendChild(previewImage);
this.currentImageElement = previewImage;
if (!this.loadedImages.includes(thumbFilename)) {
this.loadedImages.push(thumbFilename);
}
}
// Preload images before and after the current one
// Show higher quality of the same frame
// Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading
this.preloadNearby(thumbNum, true)
.then(this.preloadNearby(thumbNum, false))
.then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));
}
// Remove all preview images that aren't the designated current image
removeOldImages(currentImage) {
// Get a list of all images, convert it from a DOM list to an array
Array.from(this.currentImageContainer.children).forEach(image => {
if (image.tagName.toLowerCase() !== 'img') {
return;
}
const removeDelay = this.usingSprites ? 500 : 1000;
if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {
// Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients
// First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function
// eslint-disable-next-line no-param-reassign
image.dataset.deleting = true;
// This has to be set before the timeout - to prevent issues switching between hover and scrub
const { currentImageContainer } = this;
setTimeout(() => {
currentImageContainer.removeChild(image);
this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);
}, removeDelay);
}
});
}
// Preload images before and after the current one. Only if the user is still hovering/seeking the same frame
// This will only preload the lowest quality
preloadNearby(thumbNum, forward = true) {
return new Promise(resolve => {
setTimeout(() => {
const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;
if (this.showingThumbFilename === oldThumbFilename) {
// Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away
let thumbnailsClone;
if (forward) {
thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);
} else {
thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();
}
let foundOne = false;
thumbnailsClone.forEach(frame => {
const newThumbFilename = frame.text;
if (newThumbFilename !== oldThumbFilename) {
// Found one with a different filename. Make sure it hasn't already been loaded on this page visit
if (!this.loadedImages.includes(newThumbFilename)) {
foundOne = true;
this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);
const { urlPrefix } = this.thumbnails[0];
const thumbURL = urlPrefix + newThumbFilename;
const previewImage = new Image();
previewImage.src = thumbURL;
previewImage.onload = () => {
this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);
if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);
// We don't resolve until the thumb is loaded
resolve();
};
}
}
});
// If there are none to preload then we want to resolve immediately
if (!foundOne) {
resolve();
}
}
}, 300);
});
}
// If user has been hovering current image for half a second, look for a higher quality one
getHigherQuality(currentQualityIndex, previewImage, frame, thumbFilename) {
if (currentQualityIndex < this.thumbnails.length - 1) {
// Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container
let previewImageHeight = previewImage.naturalHeight;
if (this.usingSprites) {
previewImageHeight = frame.h;
}
if (previewImageHeight < this.thumbContainerHeight) {
// Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while
setTimeout(() => {
// Make sure the mouse hasn't already moved on and started hovering at another image
if (this.showingThumbFilename === thumbFilename) {
this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);
this.loadImage(currentQualityIndex + 1);
}
}, 300);
}
}
}
get currentImageContainer() {
if (this.mouseDown) {
return this.elements.scrubbing.container;
}
return this.elements.thumb.imageContainer;
}
get usingSprites() {
return Object.keys(this.thumbnails[0].frames[0]).includes('w');
}
get thumbAspectRatio() {
if (this.usingSprites) {
return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;
}
return this.thumbnails[0].width / this.thumbnails[0].height;
}
get thumbContainerHeight() {
if (this.mouseDown) {
const { height } = fitRatio(this.thumbAspectRatio, {
width: this.player.media.clientWidth,
height: this.player.media.clientHeight,
});
return height;
}
// If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)
if (this.sizeSpecifiedInCSS) {
return this.elements.thumb.imageContainer.clientHeight;
}
return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);
}
get currentImageElement() {
if (this.mouseDown) {
return this.currentScrubbingImageElement;
}
return this.currentThumbnailImageElement;
}
set currentImageElement(element) {
if (this.mouseDown) {
this.currentScrubbingImageElement = element;
} else {
this.currentThumbnailImageElement = element;
}
}
toggleThumbContainer(toggle = false, clearShowing = false) {
const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;
this.elements.thumb.container.classList.toggle(className, toggle);
if (!toggle && clearShowing) {
this.showingThumb = null;
this.showingThumbFilename = null;
}
}
toggleScrubbingContainer(toggle = false) {
const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;
this.elements.scrubbing.container.classList.toggle(className, toggle);
if (!toggle) {
this.showingThumb = null;
this.showingThumbFilename = null;
}
}
determineContainerAutoSizing() {
if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {
// This will prevent auto sizing in this.setThumbContainerSizeAndPos()
this.sizeSpecifiedInCSS = true;
}
}
// Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS
setThumbContainerSizeAndPos() {
if (!this.sizeSpecifiedInCSS) {
const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);
this.elements.thumb.imageContainer.style.height = `${this.thumbContainerHeight}px`;
this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`;
} else if (
this.elements.thumb.imageContainer.clientHeight > 20 &&
this.elements.thumb.imageContainer.clientWidth < 20
) {
const thumbWidth = Math.floor(this.elements.thumb.imageContainer.clientHeight * this.thumbAspectRatio);
this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`;
} else if (
this.elements.thumb.imageContainer.clientHeight < 20 &&
this.elements.thumb.imageContainer.clientWidth > 20
) {
const thumbHeight = Math.floor(this.elements.thumb.imageContainer.clientWidth / this.thumbAspectRatio);
this.elements.thumb.imageContainer.style.height = `${thumbHeight}px`;
}
this.setThumbContainerPos();
}
setThumbContainerPos() {
const seekbarRect = this.player.elements.progress.getBoundingClientRect();
const plyrRect = this.player.elements.container.getBoundingClientRect();
const { container } = this.elements.thumb;
// Find the lowest and highest desired left-position, so we don't slide out the side of the video container
const minVal = plyrRect.left - seekbarRect.left + 10;
const maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10;
// Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth
let previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2;
if (previewPos < minVal) {
previewPos = minVal;
}
if (previewPos > maxVal) {
previewPos = maxVal;
}
container.style.left = `${previewPos}px`;
}
// Can't use 100% width, in case the video is a different aspect ratio to the video container
setScrubbingContainerSize() {
const { width, height } = fitRatio(this.thumbAspectRatio, {
width: this.player.media.clientWidth,
height: this.player.media.clientHeight,
});
this.elements.scrubbing.container.style.width = `${width}px`;
this.elements.scrubbing.container.style.height = `${height}px`;
}
// Sprites need to be offset to the correct location
setImageSizeAndOffset(previewImage, frame) {
if (!this.usingSprites) {
return;
}
// Find difference between height and preview container height
const multiplier = this.thumbContainerHeight / frame.h;
// eslint-disable-next-line no-param-reassign
previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;
// eslint-disable-next-line no-param-reassign
previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;
// eslint-disable-next-line no-param-reassign
previewImage.style.left = `-${frame.x * multiplier}px`;
// eslint-disable-next-line no-param-reassign
previewImage.style.top = `-${frame.y * multiplier}px`;
}
}
export default PreviewThumbnails;
-639
View File
@@ -1,639 +0,0 @@
import { createElement } from '../utils/elements';
import { once } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import { formatTime } from '../utils/time';
// Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg"
const parseVtt = vttDataString => {
const processedList = [];
const frames = vttDataString.split(/\r\n\r\n|\n\n|\r\r/);
frames.forEach(frame => {
const result = {};
const lines = frame.split(/\r\n|\n|\r/);
lines.forEach(line => {
if (!is.number(result.startTime)) {
// The line with start and end times on it is the first line of interest
const matchTimes = line.match(
/([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,
); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT
if (matchTimes) {
result.startTime =
Number(matchTimes[1]) * 60 * 60 +
Number(matchTimes[2]) * 60 +
Number(matchTimes[3]) +
Number(`0.${matchTimes[4]}`);
result.endTime =
Number(matchTimes[6]) * 60 * 60 +
Number(matchTimes[7]) * 60 +
Number(matchTimes[8]) +
Number(`0.${matchTimes[9]}`);
}
} else if (!is.empty(line.trim()) && is.empty(result.text)) {
// If we already have the startTime, then we're definitely up to the text line(s)
const lineSplit = line.trim().split('#xywh=');
[result.text] = lineSplit;
// If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image
if (lineSplit[1]) {
[result.x, result.y, result.w, result.h] = lineSplit[1].split(',');
}
}
});
if (result.text) {
processedList.push(result);
}
});
return processedList;
};
/**
* Preview thumbnails for seek hover and scrubbing
* Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar
* Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed
*
* Notes:
* - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole
* - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails
* - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered
*/
class PreviewThumbnails {
/**
* PreviewThumbnails constructor.
* @param {Plyr} player
* @return {PreviewThumbnails}
*/
constructor(player) {
this.player = player;
this.thumbnails = [];
this.loaded = false;
this.lastMouseMoveTime = Date.now();
this.mouseDown = false;
this.loadedImages = [];
this.elements = {
thumb: {},
scrubbing: {},
};
this.load();
}
get enabled() {
return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;
}
load() {
// Togglethe regular seek tooltip
if (this.player.elements.display.seekTooltip) {
this.player.elements.display.seekTooltip.hidden = this.enabled;
}
if (!this.enabled) {
return;
}
this.getThumbnails().then(() => {
// Render DOM elements
this.render();
// Check to see if thumb container size was specified manually in CSS
this.determineContainerAutoSizing();
this.loaded = true;
});
}
// Download VTT files and parse them
getThumbnails() {
return new Promise(resolve => {
const { src } = this.player.config.previewThumbnails;
if (is.empty(src)) {
throw new Error('Missing previewThumbnails.src config attribute');
}
// If string, convert into single-element list
const urls = is.string(src) ? [src] : src;
// Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
const promises = urls.map(u => this.getThumbnail(u));
Promise.all(promises).then(() => {
// Sort smallest to biggest (e.g., [120p, 480p, 1080p])
this.thumbnails.sort((x, y) => x.height - y.height);
this.player.debug.log('Preview thumbnails', this.thumbnails);
resolve();
});
});
}
// Process individual VTT file
getThumbnail(url) {
return new Promise(resolve => {
fetch(url).then(response => {
const thumbnail = {
frames: parseVtt(response),
height: null,
urlPrefix: '',
};
// If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file
// If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank
if (!thumbnail.frames[0].text.startsWith('/')) {
thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
}
// Download the first frame, so that we can determine/set the height of this thumbnailsDef
const tempImage = new Image();
tempImage.onload = () => {
thumbnail.height = tempImage.naturalHeight;
thumbnail.width = tempImage.naturalWidth;
this.thumbnails.push(thumbnail);
resolve();
};
tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;
});
});
}
startMove(event) {
if (!this.loaded) {
return;
}
if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) {
return;
}
// Wait until media has a duration
if (!this.player.media.duration) {
return;
}
if (event.type === 'touchmove') {
// Calculate seek hover position as approx video seconds
this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);
} else {
// Calculate seek hover position as approx video seconds
const clientRect = this.player.elements.progress.getBoundingClientRect();
const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left);
this.seekTime = this.player.media.duration * (percentage / 100);
if (this.seekTime < 0) {
// The mousemove fires for 10+px out to the left
this.seekTime = 0;
}
if (this.seekTime > this.player.media.duration - 1) {
// Took 1 second off the duration for safety, because different players can disagree on the real duration of a video
this.seekTime = this.player.media.duration - 1;
}
this.mousePosX = event.pageX;
// Set time text inside image container
this.elements.thumb.time.innerText = formatTime(this.seekTime);
}
// Download and show image
this.showImageAtCurrentTime();
}
endMove() {
this.toggleThumbContainer(false, true);
}
startScrubbing(event) {
// Only act on left mouse button (0), or touch device (event.button is false)
if (event.button === false || event.button === 0) {
this.mouseDown = true;
// Wait until media has a duration
if (this.player.media.duration) {
this.toggleScrubbingContainer(true);
this.toggleThumbContainer(false, true);
// Download and show image
this.showImageAtCurrentTime();
}
}
}
endScrubbing() {
this.mouseDown = false;
// Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview
if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {
// The video was already seeked/loaded at the chosen time - hide immediately
this.toggleScrubbingContainer(false);
} else {
// The video hasn't seeked yet. Wait for that
once.call(this.player, this.player.media, 'timeupdate', () => {
// Re-check mousedown - we might have already started scrubbing again
if (!this.mouseDown) {
this.toggleScrubbingContainer(false);
}
});
}
}
/**
* Setup hooks for Plyr and window events
*/
listeners() {
// Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering
this.player.on('play', () => {
this.toggleThumbContainer(false, true);
});
this.player.on('seeked', () => {
this.toggleThumbContainer(false);
});
this.player.on('timeupdate', () => {
this.lastTime = this.player.media.currentTime;
});
}
/**
* Create HTML elements for image containers
*/
render() {
// Create HTML element: plyr__preview-thumbnail-container
this.elements.thumb.container = createElement('div', {
class: this.player.config.classNames.previewThumbnails.thumbContainer,
});
// Wrapper for the image for styling
this.elements.thumb.imageContainer = createElement('div', {
class: this.player.config.classNames.previewThumbnails.imageContainer,
});
this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);
// Create HTML element, parent+span: time text (e.g., 01:32:00)
const timeContainer = createElement('div', {
class: this.player.config.classNames.previewThumbnails.timeContainer,
});
this.elements.thumb.time = createElement('span', {}, '00:00');
timeContainer.appendChild(this.elements.thumb.time);
this.elements.thumb.container.appendChild(timeContainer);
// Inject the whole thumb
this.player.elements.progress.appendChild(this.elements.thumb.container);
// Create HTML element: plyr__preview-scrubbing-container
this.elements.scrubbing.container = createElement('div', {
class: this.player.config.classNames.previewThumbnails.scrubbingContainer,
});
this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);
}
showImageAtCurrentTime() {
if (this.mouseDown) {
this.setScrubbingContainerSize();
} else {
this.setThumbContainerSizeAndPos();
}
// Find the desired thumbnail index
// TODO: Handle a video longer than the thumbs where thumbNum is null
const thumbNum = this.thumbnails[0].frames.findIndex(
frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,
);
const hasThumb = thumbNum >= 0;
let qualityIndex = 0;
// Show the thumb container if we're not scrubbing
if (!this.mouseDown) {
this.toggleThumbContainer(hasThumb);
}
// No matching thumb found
if (!hasThumb) {
return;
}
// Check to see if we've already downloaded higher quality versions of this image
this.thumbnails.forEach((thumbnail, index) => {
if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {
qualityIndex = index;
}
});
// Only proceed if either thumbnum or thumbfilename has changed
if (thumbNum !== this.showingThumb) {
this.showingThumb = thumbNum;
this.loadImage(qualityIndex);
}
}
// Show the image that's currently specified in this.showingThumb
loadImage(qualityIndex = 0) {
const thumbNum = this.showingThumb;
const thumbnail = this.thumbnails[qualityIndex];
const { urlPrefix } = thumbnail;
const frame = thumbnail.frames[thumbNum];
const thumbFilename = thumbnail.frames[thumbNum].text;
const thumbUrl = urlPrefix + thumbFilename;
if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {
// If we're already loading a previous image, remove its onload handler - we don't want it to load after this one
// Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort
if (this.loadingImage && this.usingSprites) {
this.loadingImage.onload = null;
}
// We're building and adding a new image. In other implementations of similar functionality (YouTube), background image
// is instead used. But this causes issues with larger images in Firefox and Safari - switching between background
// images causes a flicker. Putting a new image over the top does not
const previewImage = new Image();
previewImage.src = thumbUrl;
previewImage.dataset.index = thumbNum;
previewImage.dataset.filename = thumbFilename;
this.showingThumbFilename = thumbFilename;
this.player.debug.log(`Loading image: ${thumbUrl}`);
// For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...
previewImage.onload = () =>
this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);
this.loadingImage = previewImage;
this.removeOldImages(previewImage);
} else {
// Update the existing image
this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);
this.currentImageElement.dataset.index = thumbNum;
this.removeOldImages(this.currentImageElement);
}
}
showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) {
this.player.debug.log(
`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`,
);
this.setImageSizeAndOffset(previewImage, frame);
if (newImage) {
this.currentImageContainer.appendChild(previewImage);
this.currentImageElement = previewImage;
if (!this.loadedImages.includes(thumbFilename)) {
this.loadedImages.push(thumbFilename);
}
}
// Preload images before and after the current one
// Show higher quality of the same frame
// Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading
this.preloadNearby(thumbNum, true)
.then(this.preloadNearby(thumbNum, false))
.then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));
}
// Remove all preview images that aren't the designated current image
removeOldImages(currentImage) {
// Get a list of all images, convert it from a DOM list to an array
Array.from(this.currentImageContainer.children).forEach(image => {
if (image.tagName.toLowerCase() !== 'img') {
return;
}
const removeDelay = this.usingSprites ? 500 : 1000;
if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {
// Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients
// First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function
image.dataset.deleting = true;
// This has to be set before the timeout - to prevent issues switching between hover and scrub
const { currentImageContainer } = this;
setTimeout(() => {
currentImageContainer.removeChild(image);
this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);
}, removeDelay);
}
});
}
// Preload images before and after the current one. Only if the user is still hovering/seeking the same frame
// This will only preload the lowest quality
preloadNearby(thumbNum, forward = true) {
return new Promise(resolve => {
setTimeout(() => {
const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;
if (this.showingThumbFilename === oldThumbFilename) {
// Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away
let thumbnailsClone;
if (forward) {
thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);
} else {
thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();
}
let foundOne = false;
thumbnailsClone.forEach(frame => {
const newThumbFilename = frame.text;
if (newThumbFilename !== oldThumbFilename) {
// Found one with a different filename. Make sure it hasn't already been loaded on this page visit
if (!this.loadedImages.includes(newThumbFilename)) {
foundOne = true;
this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);
const { urlPrefix } = this.thumbnails[0];
const thumbURL = urlPrefix + newThumbFilename;
const previewImage = new Image();
previewImage.src = thumbURL;
previewImage.onload = () => {
this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);
if (!this.loadedImages.includes(newThumbFilename))
this.loadedImages.push(newThumbFilename);
// We don't resolve until the thumb is loaded
resolve();
};
}
}
});
// If there are none to preload then we want to resolve immediately
if (!foundOne) {
resolve();
}
}
}, 300);
});
}
// If user has been hovering current image for half a second, look for a higher quality one
getHigherQuality(currentQualityIndex, previewImage, frame, thumbFilename) {
if (currentQualityIndex < this.thumbnails.length - 1) {
// Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container
let previewImageHeight = previewImage.naturalHeight;
if (this.usingSprites) {
previewImageHeight = frame.h;
}
if (previewImageHeight < this.thumbContainerHeight) {
// Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while
setTimeout(() => {
// Make sure the mouse hasn't already moved on and started hovering at another image
if (this.showingThumbFilename === thumbFilename) {
this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);
this.loadImage(currentQualityIndex + 1);
}
}, 300);
}
}
}
get currentImageContainer() {
if (this.mouseDown) {
return this.elements.scrubbing.container;
}
return this.elements.thumb.imageContainer;
}
get usingSprites() {
return Object.keys(this.thumbnails[0].frames[0]).includes('w');
}
get thumbAspectRatio() {
if (this.usingSprites) {
return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;
}
return this.thumbnails[0].width / this.thumbnails[0].height;
}
get thumbContainerHeight() {
if (this.mouseDown) {
// Can't use media.clientHeight - HTML5 video goes big and does black bars above and below
return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio);
}
return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);
}
get currentImageElement() {
if (this.mouseDown) {
return this.currentScrubbingImageElement;
}
return this.currentThumbnailImageElement;
}
set currentImageElement(element) {
if (this.mouseDown) {
this.currentScrubbingImageElement = element;
} else {
this.currentThumbnailImageElement = element;
}
}
toggleThumbContainer(toggle = false, clearShowing = false) {
const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;
this.elements.thumb.container.classList.toggle(className, toggle);
if (!toggle && clearShowing) {
this.showingThumb = null;
this.showingThumbFilename = null;
}
}
toggleScrubbingContainer(toggle = false) {
const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;
this.elements.scrubbing.container.classList.toggle(className, toggle);
if (!toggle) {
this.showingThumb = null;
this.showingThumbFilename = null;
}
}
determineContainerAutoSizing() {
if (this.elements.thumb.imageContainer.clientHeight > 20) {
// This will prevent auto sizing in this.setThumbContainerSizeAndPos()
this.sizeSpecifiedInCSS = true;
}
}
// Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS
setThumbContainerSizeAndPos() {
if (!this.sizeSpecifiedInCSS) {
const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);
this.elements.thumb.imageContainer.style.height = `${this.thumbContainerHeight}px`;
this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`;
}
this.setThumbContainerPos();
}
setThumbContainerPos() {
const seekbarRect = this.player.elements.progress.getBoundingClientRect();
const plyrRect = this.player.elements.container.getBoundingClientRect();
const { container } = this.elements.thumb;
// Find the lowest and highest desired left-position, so we don't slide out the side of the video container
const minVal = plyrRect.left - seekbarRect.left + 10;
const maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10;
// Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth
let previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2;
if (previewPos < minVal) {
previewPos = minVal;
}
if (previewPos > maxVal) {
previewPos = maxVal;
}
container.style.left = `${previewPos}px`;
}
// Can't use 100% width, in case the video is a different aspect ratio to the video container
setScrubbingContainerSize() {
this.elements.scrubbing.container.style.width = `${this.player.media.clientWidth}px`;
// Can't use media.clientHeight - html5 video goes big and does black bars above and below
this.elements.scrubbing.container.style.height = `${this.player.media.clientWidth / this.thumbAspectRatio}px`;
}
// Sprites need to be offset to the correct location
setImageSizeAndOffset(previewImage, frame) {
if (!this.usingSprites) {
return;
}
// Find difference between height and preview container height
const multiplier = this.thumbContainerHeight / frame.h;
previewImage.style.height = `${Math.floor(previewImage.naturalHeight * multiplier)}px`;
previewImage.style.width = `${Math.floor(previewImage.naturalWidth * multiplier)}px`;
previewImage.style.left = `-${frame.x * multiplier}px`;
previewImage.style.top = `-${frame.y * multiplier}px`;
}
}
export default PreviewThumbnails;
+51 -31
View File
@@ -9,8 +9,7 @@ import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events'; import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch'; import fetch from '../utils/fetch';
import is from '../utils/is'; import is from '../utils/is';
import loadScript from '../utils/loadScript'; import loadScript from '../utils/load-script';
import { extend } from '../utils/objects';
import { format, stripHTML } from '../utils/strings'; import { format, stripHTML } from '../utils/strings';
import { setAspectRatio } from '../utils/style'; import { setAspectRatio } from '../utils/style';
import { buildUrlParams } from '../utils/urls'; import { buildUrlParams } from '../utils/urls';
@@ -42,23 +41,28 @@ function assurePlaybackState(play) {
const vimeo = { const vimeo = {
setup() { setup() {
const player = this;
// Add embed class for responsive // Add embed class for responsive
toggleClass(this.elements.wrapper, this.config.classNames.embed, true); toggleClass(player.elements.wrapper, player.config.classNames.embed, true);
// Set speed options from config
player.options.speed = player.config.speed.options;
// Set intial ratio // Set intial ratio
setAspectRatio.call(this); setAspectRatio.call(player);
// Load the API if not already // Load the SDK if not already
if (!is.object(window.Vimeo)) { if (!is.object(window.Vimeo)) {
loadScript(this.config.urls.vimeo.sdk) loadScript(player.config.urls.vimeo.sdk)
.then(() => { .then(() => {
vimeo.ready.call(this); vimeo.ready.call(player);
}) })
.catch(error => { .catch(error => {
this.debug.warn('Vimeo API failed to load', error); player.debug.warn('Vimeo SDK (player.js) failed to load', error);
}); });
} else { } else {
vimeo.ready.call(this); vimeo.ready.call(player);
} }
}, },
@@ -66,21 +70,25 @@ const vimeo = {
ready() { ready() {
const player = this; const player = this;
const config = player.config.vimeo; const config = player.config.vimeo;
const { premium, referrerPolicy, ...frameParams } = config;
// If the owner has a pro or premium account then we can hide controls etc
if (premium) {
Object.assign(frameParams, {
controls: false,
sidedock: false,
});
}
// Get Vimeo params for the iframe // Get Vimeo params for the iframe
const params = buildUrlParams( const params = buildUrlParams({
extend(
{},
{
loop: player.config.loop.active, loop: player.config.loop.active,
autoplay: player.autoplay, autoplay: player.autoplay,
muted: player.muted, muted: player.muted,
gesture: 'media', gesture: 'media',
playsinline: !this.config.fullscreen.iosNative, playsinline: !this.config.fullscreen.iosNative,
}, ...frameParams,
config, });
),
);
// Get the source URL or ID // Get the source URL or ID
let source = player.media.getAttribute('src'); let source = player.media.getAttribute('src');
@@ -91,22 +99,28 @@ const vimeo = {
} }
const id = parseId(source); const id = parseId(source);
// Build an iframe // Build an iframe
const iframe = createElement('iframe'); const iframe = createElement('iframe');
const src = format(player.config.urls.vimeo.iframe, id, params); const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src); iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', ''); iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', ''); iframe.setAttribute('allow', 'autoplay,fullscreen,picture-in-picture');
iframe.setAttribute('allow', 'autoplay');
// Get poster, if already set // Set the referrer policy if required
const { poster } = player; if (!is.empty(referrerPolicy)) {
iframe.setAttribute('referrerPolicy', referrerPolicy);
}
// Inject the package // Inject the package
const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer }); const { poster } = player;
if (premium) {
iframe.setAttribute('data-poster', poster);
player.media = replaceElement(iframe, player.media);
} else {
const wrapper = createElement('div', { class: player.config.classNames.embedContainer, 'data-poster': poster });
wrapper.appendChild(iframe); wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media); player.media = replaceElement(wrapper, player.media);
}
// Get poster image // Get poster image
fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => { fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
@@ -199,11 +213,9 @@ const vimeo = {
speed = input; speed = input;
triggerEvent.call(player, player.media, 'ratechange'); triggerEvent.call(player, player.media, 'ratechange');
}) })
.catch(error => { .catch(() => {
// Hide menu item (and menu if empty) // Cannot set Playback Rate, Video is probably not on Pro account
if (error.name === 'Error') { player.options.speed = [1];
controls.setSpeedMenu.call(player, []);
}
}); });
}, },
}); });
@@ -259,7 +271,7 @@ const vimeo = {
.getVideoUrl() .getVideoUrl()
.then(value => { .then(value => {
currentSrc = value; currentSrc = value;
controls.setDownloadLink.call(player); controls.setDownloadUrl.call(player);
}) })
.catch(error => { .catch(error => {
this.debug.warn(error); this.debug.warn(error);
@@ -281,8 +293,8 @@ const vimeo = {
// Set aspect ratio based on video size // Set aspect ratio based on video size
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => { Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
const [width, height] = dimensions; const [width, height] = dimensions;
player.embed.ratio = `${width}:${height}`; player.embed.ratio = [width, height];
setAspectRatio.call(this, player.embed.ratio); setAspectRatio.call(this);
}); });
// Set autopause // Set autopause
@@ -337,6 +349,14 @@ const vimeo = {
} }
}); });
player.embed.on('bufferstart', () => {
triggerEvent.call(player, player.media, 'waiting');
});
player.embed.on('bufferend', () => {
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('play', () => { player.embed.on('play', () => {
assurePlaybackState.call(player, true); assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing'); triggerEvent.call(player, player.media, 'playing');
+59 -53
View File
@@ -7,8 +7,8 @@ import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events'; import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch'; import fetch from '../utils/fetch';
import is from '../utils/is'; import is from '../utils/is';
import loadImage from '../utils/loadImage'; import loadImage from '../utils/load-image';
import loadScript from '../utils/loadScript'; import loadScript from '../utils/load-script';
import { extend } from '../utils/objects'; import { extend } from '../utils/objects';
import { format, generateId } from '../utils/strings'; import { format, generateId } from '../utils/strings';
import { setAspectRatio } from '../utils/style'; import { setAspectRatio } from '../utils/style';
@@ -34,78 +34,78 @@ function assurePlaybackState(play) {
} }
} }
function getHost(config) {
if (config.noCookie) {
return 'https://www.youtube-nocookie.com';
}
if (window.location.protocol === 'http:') {
return 'http://www.youtube.com';
}
// Use YouTube's default
return undefined;
}
const youtube = { const youtube = {
setup() { setup() {
// Add embed class for responsive // Add embed class for responsive
toggleClass(this.elements.wrapper, this.config.classNames.embed, true); toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set aspect ratio
setAspectRatio.call(this);
// Setup API // Setup API
if (is.object(window.YT) && is.function(window.YT.Player)) { if (is.object(window.YT) && is.function(window.YT.Player)) {
youtube.ready.call(this); youtube.ready.call(this);
} else { } else {
// Load the API // Reference current global callback
loadScript(this.config.urls.youtube.sdk).catch(error => { const callback = window.onYouTubeIframeAPIReady;
this.debug.warn('YouTube API failed to load', error);
});
// Setup callback for the API
// YouTube has it's own system of course...
window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || [];
// Add to queue
window.onYouTubeReadyCallbacks.push(() => {
youtube.ready.call(this);
});
// Set callback to process queue // Set callback to process queue
window.onYouTubeIframeAPIReady = () => { window.onYouTubeIframeAPIReady = () => {
window.onYouTubeReadyCallbacks.forEach(callback => { // Call global callback if set
if (is.function(callback)) {
callback(); callback();
}); }
youtube.ready.call(this);
}; };
// Load the SDK
loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
} }
}, },
// Get the media title // Get the media title
getTitle(videoId) { getTitle(videoId) {
// Try via undocumented API method first const url = format(this.config.urls.youtube.api, videoId);
// This method disappears now and then though...
// https://github.com/sampotts/plyr/issues/709
if (is.function(this.embed.getVideoData)) {
const { title } = this.embed.getVideoData();
if (is.empty(title)) {
this.config.title = title;
ui.setTitle.call(this);
return;
}
}
// Or via Google API
const key = this.config.keys.google;
if (is.string(key) && !is.empty(key)) {
const url = format(this.config.urls.youtube.api, videoId, key);
fetch(url) fetch(url)
.then(result => { .then(data => {
if (is.object(result)) { if (is.object(data)) {
this.config.title = result.items[0].snippet.title; const { title, height, width } = data;
// Set title
this.config.title = title;
ui.setTitle.call(this); ui.setTitle.call(this);
// Set aspect ratio
this.embed.ratio = [width, height];
} }
setAspectRatio.call(this);
}) })
.catch(() => {}); .catch(() => {
} // Set aspect ratio
setAspectRatio.call(this);
});
}, },
// API ready // API ready
ready() { ready() {
const player = this; const player = this;
// Ignore already setup (race condition) // Ignore already setup (race condition)
const currentId = player.media.getAttribute('id'); const currentId = player.media && player.media.getAttribute('id');
if (!is.empty(currentId) && currentId.startsWith('youtube-')) { if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return; return;
} }
@@ -121,25 +121,23 @@ const youtube = {
// Replace the <iframe> with a <div> due to YouTube API issues // Replace the <iframe> with a <div> due to YouTube API issues
const videoId = parseId(source); const videoId = parseId(source);
const id = generateId(player.provider); const id = generateId(player.provider);
// Get poster, if already set // Get poster, if already set
const { poster } = player; const { poster } = player;
// Replace media element // Replace media element
const container = createElement('div', { id, poster }); const container = createElement('div', { id, 'data-poster': poster });
player.media = replaceElement(container, player.media); player.media = replaceElement(container, player.media);
// Id to poster wrapper // Id to poster wrapper
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`; const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide) // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3 .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
.then(image => ui.setPoster.call(player, image.src)) .then(image => ui.setPoster.call(player, image.src))
.then(posterSrc => { .then(src => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters) // If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!posterSrc.includes('maxres')) { if (!src.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover'; player.elements.poster.style.backgroundSize = 'cover';
} }
}) })
@@ -151,7 +149,7 @@ const youtube = {
// https://developers.google.com/youtube/iframe_api_reference // https://developers.google.com/youtube/iframe_api_reference
player.embed = new window.YT.Player(id, { player.embed = new window.YT.Player(id, {
videoId, videoId,
host: config.noCookie ? 'https://www.youtube-nocookie.com' : undefined, host: getHost(config),
playerVars: extend( playerVars: extend(
{}, {},
{ {
@@ -299,7 +297,9 @@ const youtube = {
}); });
// Get available speeds // Get available speeds
player.options.speed = instance.getAvailablePlaybackRates(); const speeds = instance.getAvailablePlaybackRates();
// Filter based on config
player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));
// Set the tabindex to avoid focus entering iframe // Set the tabindex to avoid focus entering iframe
if (player.supported.ui) { if (player.supported.ui) {
@@ -386,7 +386,7 @@ const youtube = {
case 1: case 1:
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet) // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (player.media.paused && !player.embed.hasPlayed) { if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
player.media.pause(); player.media.pause();
} else { } else {
assurePlaybackState.call(player, true); assurePlaybackState.call(player, true);
@@ -418,6 +418,12 @@ const youtube = {
break; break;
case 3:
// Trigger waiting event to add loading classes to container as the video buffers.
triggerEvent.call(player, player.media, 'waiting');
break;
default: default:
break; break;
} }
+630
View File
@@ -0,0 +1,630 @@
// Type definitions for plyr 3.5
// Project: https://plyr.io
// Definitions by: ondratra <https://github.com/ondratra>
// TypeScript Version: 3.0
export = Plyr;
export as namespace Plyr;
declare class Plyr {
/**
* Setup a new instance
*/
static setup(targets: NodeList | HTMLElement | HTMLElement[] | string, options?: Plyr.Options): Plyr[];
/**
* Check for support
* @param mediaType
* @param provider
* @param playsInline Whether the player has the playsinline attribute (only applicable to iOS 10+)
*/
static supported(mediaType?: Plyr.MediaType, provider?: Plyr.Provider, playsInline?: boolean): Plyr.Support;
constructor(targets: NodeList | HTMLElement | HTMLElement[] | string, options?: Plyr.Options);
/**
* Indicates if the current player is HTML5.
*/
readonly isHTML5: boolean;
/**
* Indicates if the current player is an embedded player.
*/
readonly isEmbed: boolean;
/**
* Indicates if the current player is playing.
*/
readonly playing: boolean;
/**
* Indicates if the current player is paused.
*/
readonly paused: boolean;
/**
* Indicates if the current player is stopped.
*/
readonly stopped: boolean;
/**
* Indicates if the current player has finished playback.
*/
readonly ended: boolean;
/**
* Returns a float between 0 and 1 indicating how much of the media is buffered
*/
readonly buffered: number;
/**
* Gets or sets the currentTime for the player. The setter accepts a float in seconds.
*/
currentTime: number;
/**
* Indicates if the current player is seeking.
*/
readonly seeking: boolean;
/**
* Returns the duration for the current media.
*/
readonly duration: number;
/**
* Gets or sets the volume for the player. The setter accepts a float between 0 and 1.
*/
volume: number;
/**
* Gets or sets the muted state of the player. The setter accepts a boolean.
*/
muted: boolean;
/**
* Indicates if the current media has an audio track.
*/
readonly hasAudio: boolean;
/**
* Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5.
*/
speed: number;
/**
* Gets or sets the quality for the player. The setter accepts a value from the options specified in your config.
*/
quality: number;
/**
* Gets or sets the current loop state of the player.
*/
loop: boolean;
/**
* Gets or sets the current source for the player.
*/
source: Plyr.SourceInfo;
/**
* Gets or sets the current poster image URL for the player.
*/
poster: string;
/**
* Gets or sets the autoplay state of the player.
*/
autoplay: boolean;
/**
* Gets or sets the caption track by index. 1 means the track is missing or captions is not active
*/
currentTrack: number;
/**
* Gets or sets the preferred captions language for the player. The setter accepts an ISO twoletter language code. Support for the languages is dependent on the captions you include.
* If your captions don't have any language data, or if you have multiple tracks with the same language, you may want to use currentTrack instead.
*/
language: string;
/**
* Gets or sets the picture-in-picture state of the player. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+.
*/
pip: boolean;
/**
* Gets or sets the aspect ratio for embedded players.
*/
ratio?: string;
/**
* Returns the current video Provider
*/
readonly provider: 'html5' | 'vimeo' | 'youtube';
/**
* Returns the native API for Vimeo or Youtube players
*/
readonly embed?: any;
readonly fullscreen: Plyr.FullscreenControl;
/**
* Start playback.
* For HTML5 players, play() will return a Promise in some browsers - WebKit and Mozilla according to MDN at time of writing.
*/
play(): Promise<void> | void;
/**
* Pause playback.
*/
pause(): void;
/**
* Toggle playback, if no parameters are passed, it will toggle based on current status.
*/
togglePlay(toggle?: boolean): boolean;
/**
* Stop playback and reset to start.
*/
stop(): void;
/**
* Restart playback.
*/
restart(): void;
/**
* Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used.
*/
rewind(seekTime?: number): void;
/**
* Fast forward by the specified seek time. If no parameter is passed, the default seek time will be used.
*/
forward(seekTime?: number): void;
/**
* Increase volume by the specified step. If no parameter is passed, the default step will be used.
*/
increaseVolume(step?: number): void;
/**
* Increase volume by the specified step. If no parameter is passed, the default step will be used.
*/
decreaseVolume(step?: number): void;
/**
* Toggle captions display. If no parameter is passed, it will toggle based on current status.
*/
toggleCaptions(toggle?: boolean): void;
/**
* Trigger the airplay dialog on supported devices.
*/
airplay(): void;
/**
* Toggle the controls (video only). Takes optional truthy value to force it on/off.
*/
toggleControls(toggle: boolean): void;
/**
* Add an event listener for the specified event.
*/
on(
event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
callback: (this: this, event: Plyr.PlyrEvent) => void,
): void;
/**
* Add an event listener for the specified event once.
*/
once(
event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
callback: (this: this, event: Plyr.PlyrEvent) => void,
): void;
/**
* Remove an event listener for the specified event.
*/
off(
event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
callback: (this: this, event: Plyr.PlyrEvent) => void,
): void;
/**
* Check support for a mime type.
*/
supports(type: string): boolean;
/**
* Destroy lib instance
*/
destroy(): void;
}
declare namespace Plyr {
type MediaType = 'audio' | 'video';
type Provider = 'html5' | 'youtube' | 'vimeo';
type StandardEvent =
| 'progress'
| 'playing'
| 'play'
| 'pause'
| 'timeupdate'
| 'volumechange'
| 'seeking'
| 'seeked'
| 'ratechange'
| 'ended'
| 'enterfullscreen'
| 'exitfullscreen'
| 'captionsenabled'
| 'captionsdisabled'
| 'languagechange'
| 'controlshidden'
| 'controlsshown'
| 'ready';
type Html5Event =
| 'loadstart'
| 'loadeddata'
| 'loadedmetadata'
| 'canplay'
| 'canplaythrough'
| 'stalled'
| 'waiting'
| 'emptied'
| 'cuechange'
| 'error';
type YoutubeEvent = 'statechange' | 'qualitychange' | 'qualityrequested';
interface FullscreenControl {
/**
* Indicates if the current player is in fullscreen mode.
*/
readonly active: boolean;
/**
* Indicates if the current player has fullscreen enabled.
*/
readonly enabled: boolean;
/**
* Enter fullscreen. If fullscreen is not supported, a fallback ""full window/viewport"" is used instead.
*/
enter(): void;
/**
* Exit fullscreen.
*/
exit(): void;
/**
* Toggle fullscreen.
*/
toggle(): void;
}
interface Options {
/**
* Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below.
*/
enabled?: boolean;
/**
* Display debugging information in the console
*/
debug?: boolean;
/**
* If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function;
* id (the unique id for the player), seektime (the seektime step in seconds), and title (the media title). See CONTROLS.md for more info on how the html needs to be structured.
* Defaults to ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']
*/
controls?: string[] | ((id: string, seektime: number, title: string) => unknown) | Element;
/**
* If you're using the default controls are used then you can specify which settings to show in the menu
* Defaults to ['captions', 'quality', 'speed', 'loop']
*/
settings?: string[];
/**
* Used for internationalization (i18n) of the text within the UI.
*/
i18n?: any;
/**
* Load the SVG sprite specified as the iconUrl option (if a URL). If false, it is assumed you are handling sprite loading yourself.
*/
loadSprite?: boolean;
/**
* Specify a URL or path to the SVG sprite. See the SVG section for more info.
*/
iconUrl?: string;
/**
* Specify the id prefix for the icons used in the default controls (e.g. plyr-play would be plyr).
* This is to prevent clashes if you're using your own SVG sprite but with the default controls.
* Most people can ignore this option.
*/
iconPrefix?: string;
/**
* Specify a URL or path to a blank video file used to properly cancel network requests.
*/
blankUrl?: string;
/**
* Autoplay the media on load. This is generally advised against on UX grounds. It is also disabled by default in some browsers.
* If the autoplay attribute is present on a <video> or <audio> element, this will be automatically set to true.
*/
autoplay?: boolean;
/**
* Only allow one player playing at once.
*/
autopause?: boolean;
/**
* The time, in seconds, to seek when a user hits fast forward or rewind.
*/
seekTime?: number;
/**
* A number, between 0 and 1, representing the initial volume of the player.
*/
volume?: number;
/**
* Whether to start playback muted. If the muted attribute is present on a <video> or <audio> element, this will be automatically set to true.
*/
muted?: boolean;
/**
* Click (or tap) of the video container will toggle play/pause.
*/
clickToPlay?: boolean;
/**
* Disable right click menu on video to help as very primitive obfuscation to prevent downloads of content.
*/
disableContextMenu?: boolean;
/**
* Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen.
* As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly.
*/
hideControls?: boolean;
/**
* Reset the playback to the start once playback is complete.
*/
resetOnEnd?: boolean;
/**
* Enable keyboard shortcuts for focused players only or globally
*/
keyboard?: KeyboardOptions;
/**
* controls: Display control labels as tooltips on :hover & :focus (by default, the labels are screen reader only).
* seek: Display a seek tooltip to indicate on click where the media would seek to.
*/
tooltips?: TooltipOptions;
/**
* Specify a custom duration for media.
*/
duration?: number;
/**
* Displays the duration of the media on the metadataloaded event (on startup) in the current time display.
* This will only work if the preload attribute is not set to none (or is not set at all) and you choose not to display the duration (see controls option).
*/
displayDuration?: boolean;
/**
* Display the current time as a countdown rather than an incremental counter.
*/
invertTime?: boolean;
/**
* Allow users to click to toggle the above.
*/
toggleInvert?: boolean;
/**
* Allows binding of event listeners to the controls before the default handlers. See the defaults.js for available listeners.
* If your handler prevents default on the event (event.preventDefault()), the default handler will not fire.
*/
listeners?: { [key: string]: (error: PlyrEvent) => void };
/**
* active: Toggles if captions should be active by default. language: Sets the default language to load (if available). 'auto' uses the browser language.
* update: Listen to changes to tracks and update menu. This is needed for some streaming libraries, but can result in unselectable language options).
*/
captions?: CaptionOptions;
/**
* enabled: Toggles whether fullscreen should be enabled. fallback: Allow fallback to a full-window solution.
* iosNative: whether to use native iOS fullscreen when entering fullscreen (no custom controls)
*/
fullscreen?: FullScreenOptions;
/**
* The aspect ratio you want to use for embedded players.
*/
ratio?: string;
/**
* enabled: Allow use of local storage to store user settings. key: The key name to use.
*/
storage?: StorageOptions;
/**
* selected: The default speed for playback. options: The speed options to display in the UI. YouTube and Vimeo will ignore any options outside of the 0.5-2 range, so options outside of this range will be hidden automatically.
*/
speed?: SpeedOptions;
/**
* Currently only supported by YouTube. default is the default quality level, determined by YouTube. options are the options to display.
*/
quality?: QualityOptions;
/**
* active: Whether to loop the current video. If the loop attribute is present on a <video> or <audio> element,
* this will be automatically set to true This is an object to support future functionality.
*/
loop?: LoopOptions;
/**
* enabled: Whether to enable vi.ai ads. publisherId: Your unique vi.ai publisher ID.
*/
ads?: AdOptions;
/**
* Vimeo Player Options.
*/
vimeo?: object;
/**
* Youtube Player Options.
*/
youtube?: object;
/**
* Preview Thumbnails Options.
*/
previewThumbnails?: PreviewThumbnailsOptions;
}
interface QualityOptions {
default: number;
options: number[];
}
interface LoopOptions {
active: boolean;
}
interface AdOptions {
enabled: boolean;
publisherId: string;
}
interface SpeedOptions {
selected: number;
options: number[];
}
interface KeyboardOptions {
focused?: boolean;
global?: boolean;
}
interface TooltipOptions {
controls?: boolean;
seek?: boolean;
}
interface FullScreenOptions {
enabled?: boolean;
fallback?: boolean;
allowAudio?: boolean;
iosNative?: boolean;
}
interface CaptionOptions {
active?: boolean;
language?: string;
update?: boolean;
}
interface StorageOptions {
enabled?: boolean;
key?: string;
}
interface PreviewThumbnailsOptions {
enabled?: boolean;
src?: string;
}
interface SourceInfo {
/**
* Note: YouTube and Vimeo are currently not supported as audio sources.
*/
type: MediaType;
/**
* Title of the new media. Used for the aria-label attribute on the play button, and outer container. YouTube and Vimeo are populated automatically.
*/
title?: string;
/**
* This is an array of sources. For HTML5 media, the properties of this object are mapped directly to HTML attributes so more can be added to the object if required.
*/
sources: Source[];
/**
* The URL for the poster image (HTML5 video only).
*/
poster?: string;
/**
* An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above,
* it will render as <track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default> and similar for the French version.
* Booleans are converted to HTML5 value-less attributes.
*/
tracks?: Track[];
}
interface Source {
/**
* The URL of the media file (or YouTube/Vimeo URL).
*/
src: string;
/**
* The MIME type of the media file (if HTML5).
*/
type?: string;
provider?: Provider;
size?: number;
}
type TrackKind = 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
interface Track {
/**
* Indicates how the text track is meant to be used
*/
kind: TrackKind;
/**
* Indicates a user-readable title for the track
*/
label: string;
/**
* The language of the track text data. It must be a valid BCP 47 language tag. If the kind attribute is set to subtitles, then srclang must be defined.
*/
srcLang?: string;
/**
* The URL of the track (.vtt file).
*/
src: string;
default?: boolean;
}
interface PlyrEvent extends CustomEvent {
readonly detail: { readonly plyr: Plyr };
}
interface Support {
api: boolean;
ui: boolean;
}
}
+122 -43
View File
@@ -1,6 +1,6 @@
// ========================================================================== // ==========================================================================
// Plyr // Plyr
// plyr.js v3.5.2 // plyr.js v3.6.2
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================
@@ -15,7 +15,7 @@ import Fullscreen from './fullscreen';
import Listeners from './listeners'; import Listeners from './listeners';
import media from './media'; import media from './media';
import Ads from './plugins/ads'; import Ads from './plugins/ads';
import PreviewThumbnails from './plugins/previewThumbnails'; import PreviewThumbnails from './plugins/preview-thumbnails';
import source from './source'; import source from './source';
import Storage from './storage'; import Storage from './storage';
import support from './support'; import support from './support';
@@ -24,8 +24,11 @@ import { closest } from './utils/arrays';
import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements'; import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';
import { off, on, once, triggerEvent, unbindListeners } from './utils/events'; import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
import is from './utils/is'; import is from './utils/is';
import loadSprite from './utils/loadSprite'; import loadSprite from './utils/load-sprite';
import { clamp } from './utils/numbers';
import { cloneDeep, extend } from './utils/objects'; import { cloneDeep, extend } from './utils/objects';
import { silencePromise } from './utils/promise';
import { getAspectRatio, reduceAspectRatio, setAspectRatio, validateRatio } from './utils/style';
import { parseUrl } from './utils/urls'; import { parseUrl } from './utils/urls';
// Private properties // Private properties
@@ -77,6 +80,7 @@ class Plyr {
// Elements cache // Elements cache
this.elements = { this.elements = {
container: null, container: null,
fullscreen: null,
captions: null, captions: null,
buttons: {}, buttons: {},
display: {}, display: {},
@@ -149,7 +153,6 @@ class Plyr {
// Set media type based on tag or data attribute // Set media type based on tag or data attribute
// Supported: video, audio, vimeo, youtube // Supported: video, audio, vimeo, youtube
const type = this.media.tagName.toLowerCase(); const type = this.media.tagName.toLowerCase();
// Embed properties // Embed properties
let iframe = null; let iframe = null;
let url = null; let url = null;
@@ -267,6 +270,9 @@ class Plyr {
wrap(this.media, this.elements.container); wrap(this.media, this.elements.container);
} }
// Migrate custom properties from media to container (so they work 😉)
ui.migrateStyles.call(this);
// Add style hook // Add style hook
ui.addStyleHook.call(this); ui.addStyleHook.call(this);
@@ -280,6 +286,9 @@ class Plyr {
}); });
} }
// Setup fullscreen
this.fullscreen = new Fullscreen(this);
// Setup interface // Setup interface
// If embed but not fully supported, build interface now to avoid flash of controls // If embed but not fully supported, build interface now to avoid flash of controls
if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) { if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {
@@ -292,17 +301,14 @@ class Plyr {
// Global listeners // Global listeners
this.listeners.global(); this.listeners.global();
// Setup fullscreen
this.fullscreen = new Fullscreen(this);
// Setup ads if provided // Setup ads if provided
if (this.config.ads.enabled) { if (this.config.ads.enabled) {
this.ads = new Ads(this); this.ads = new Ads(this);
} }
// Autoplay if required // Autoplay if required
if (this.config.autoplay) { if (this.isHTML5 && this.config.autoplay) {
this.play(); setTimeout(() => silencePromise(this.play()), 10);
} }
// Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
@@ -322,27 +328,27 @@ class Plyr {
* Types and provider helpers * Types and provider helpers
*/ */
get isHTML5() { get isHTML5() {
return Boolean(this.provider === providers.html5); return this.provider === providers.html5;
} }
get isEmbed() { get isEmbed() {
return Boolean(this.isYouTube || this.isVimeo); return this.isYouTube || this.isVimeo;
} }
get isYouTube() { get isYouTube() {
return Boolean(this.provider === providers.youtube); return this.provider === providers.youtube;
} }
get isVimeo() { get isVimeo() {
return Boolean(this.provider === providers.vimeo); return this.provider === providers.vimeo;
} }
get isVideo() { get isVideo() {
return Boolean(this.type === types.video); return this.type === types.video;
} }
get isAudio() { get isAudio() {
return Boolean(this.type === types.audio); return this.type === types.audio;
} }
/** /**
@@ -355,7 +361,7 @@ class Plyr {
// Intecept play with ads // Intecept play with ads
if (this.ads && this.ads.enabled) { if (this.ads && this.ads.enabled) {
this.ads.managerPromise.then(() => this.ads.play()).catch(() => this.media.play()); this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));
} }
// Return the promise (for HTML5) // Return the promise (for HTML5)
@@ -367,10 +373,10 @@ class Plyr {
*/ */
pause() { pause() {
if (!this.playing || !is.function(this.media.pause)) { if (!this.playing || !is.function(this.media.pause)) {
return; return null;
} }
this.media.pause(); return this.media.pause();
} }
/** /**
@@ -410,10 +416,10 @@ class Plyr {
const toggle = is.boolean(input) ? input : !this.playing; const toggle = is.boolean(input) ? input : !this.playing;
if (toggle) { if (toggle) {
this.play(); return this.play();
} else {
this.pause();
} }
return this.pause();
} }
/** /**
@@ -440,7 +446,7 @@ class Plyr {
* @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
*/ */
rewind(seekTime) { rewind(seekTime) {
this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime); this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;
} }
/** /**
@@ -448,7 +454,7 @@ class Plyr {
* @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
*/ */
forward(seekTime) { forward(seekTime) {
this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime); this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;
} }
/** /**
@@ -512,7 +518,6 @@ class Plyr {
get duration() { get duration() {
// Faux duration set via config // Faux duration set via config
const fauxDuration = parseFloat(this.config.duration); const fauxDuration = parseFloat(this.config.duration);
// Media duration can be NaN or Infinity before the media has loaded // Media duration can be NaN or Infinity before the media has loaded
const realDuration = (this.media || {}).duration; const realDuration = (this.media || {}).duration;
const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration; const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;
@@ -660,24 +665,17 @@ class Plyr {
speed = this.config.speed.selected; speed = this.config.speed.selected;
} }
// Set min/max // Clamp to min/max
if (speed < 0.1) { const { minimumSpeed: min, maximumSpeed: max } = this;
speed = 0.1; speed = clamp(speed, min, max);
}
if (speed > 2.0) {
speed = 2.0;
}
if (!this.config.speed.options.includes(speed)) {
this.debug.warn(`Unsupported speed (${speed})`);
return;
}
// Update config // Update config
this.config.speed.selected = speed; this.config.speed.selected = speed;
// Set media speed // Set media speed
setTimeout(() => {
this.media.playbackRate = speed; this.media.playbackRate = speed;
}, 0);
} }
/** /**
@@ -687,6 +685,42 @@ class Plyr {
return Number(this.media.playbackRate); return Number(this.media.playbackRate);
} }
/**
* Get the minimum allowed speed
*/
get minimumSpeed() {
if (this.isYouTube) {
// https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
return Math.min(...this.options.speed);
}
if (this.isVimeo) {
// https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
return 0.5;
}
// https://stackoverflow.com/a/32320020/1191319
return 0.0625;
}
/**
* Get the maximum allowed speed
*/
get maximumSpeed() {
if (this.isYouTube) {
// https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
return Math.max(...this.options.speed);
}
if (this.isVimeo) {
// https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
return 2;
}
// https://stackoverflow.com/a/32320020/1191319
return 16;
}
/** /**
* Set playback quality * Set playback quality
* Currently HTML5 & YouTube only * Currently HTML5 & YouTube only
@@ -822,6 +856,19 @@ class Plyr {
return is.url(download) ? download : this.source; return is.url(download) ? download : this.source;
} }
/**
* Set the download URL
*/
set download(input) {
if (!is.url(input)) {
return;
}
this.config.urls.download = input;
controls.setDownloadUrl.call(this);
}
/** /**
* Set the poster image for a video * Set the poster image for a video
* @param {String} input - the URL for the new poster image * @param {String} input - the URL for the new poster image
@@ -843,7 +890,39 @@ class Plyr {
return null; return null;
} }
return this.media.getAttribute('poster'); return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');
}
/**
* Get the current aspect ratio in use
*/
get ratio() {
if (!this.isVideo) {
return null;
}
const ratio = reduceAspectRatio(getAspectRatio.call(this));
return is.array(ratio) ? ratio.join(':') : ratio;
}
/**
* Set video aspect ratio
*/
set ratio(input) {
if (!this.isVideo) {
this.debug.warn('Aspect ratio can only be set for video');
return;
}
if (!is.string(input) || !validateRatio(input)) {
this.debug.error(`Invalid aspect ratio specified (${input})`);
return;
}
this.config.ratio = input;
setAspectRatio.call(this);
} }
/** /**
@@ -969,15 +1048,13 @@ class Plyr {
if (this.supported.ui && !this.isAudio) { if (this.supported.ui && !this.isAudio) {
// Get state before change // Get state before change
const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls); const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);
// Negate the argument if not undefined since adding the class to hides the controls // Negate the argument if not undefined since adding the class to hides the controls
const force = typeof toggle === 'undefined' ? undefined : !toggle; const force = typeof toggle === 'undefined' ? undefined : !toggle;
// Apply and get updated state // Apply and get updated state
const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force); const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);
// Close menu // Close menu
if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) { if (hiding && is.array(this.config.controls) && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
controls.toggleMenu.call(this, false); controls.toggleMenu.call(this, false);
} }
@@ -1088,11 +1165,13 @@ class Plyr {
// Stop playback // Stop playback
this.stop(); this.stop();
// Clear timeouts
clearTimeout(this.timers.loading);
clearTimeout(this.timers.controls);
clearTimeout(this.timers.resized);
// Provider specific stuff // Provider specific stuff
if (this.isHTML5) { if (this.isHTML5) {
// Clear timeout
clearTimeout(this.timers.loading);
// Restore native video controls // Restore native video controls
ui.toggleNativeControls.call(this, true); ui.toggleNativeControls.call(this, true);
+2 -1
View File
@@ -1,12 +1,13 @@
// ========================================================================== // ==========================================================================
// Plyr Polyfilled Build // Plyr Polyfilled Build
// plyr.js v3.5.2 // plyr.js v3.6.2
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================
import 'custom-event-polyfill'; import 'custom-event-polyfill';
import 'url-polyfill'; import 'url-polyfill';
import Plyr from './plyr'; import Plyr from './plyr';
export default Plyr; export default Plyr;
+15 -3
View File
@@ -5,6 +5,7 @@
import { providers } from './config/types'; import { providers } from './config/types';
import html5 from './html5'; import html5 from './html5';
import media from './media'; import media from './media';
import PreviewThumbnails from './plugins/preview-thumbnails';
import support from './support'; import support from './support';
import ui from './ui'; import ui from './ui';
import { createElement, insertElement, removeElement } from './utils/elements'; import { createElement, insertElement, removeElement } from './utils/elements';
@@ -130,9 +131,20 @@ const source = {
this.media.load(); this.media.load();
} }
// Reload thumbnails // Update previewThumbnails config & reload plugin
if (this.previewThumbnails) { if (!is.empty(input.previewThumbnails)) {
this.previewThumbnails.load(); Object.assign(this.config.previewThumbnails, input.previewThumbnails);
// Cleanup previewThumbnails plugin if it was loaded
if (this.previewThumbnails && this.previewThumbnails.loaded) {
this.previewThumbnails.destroy();
this.previewThumbnails = null;
}
// Create new instance if it is still enabled
if (this.config.previewThumbnails.enabled) {
this.previewThumbnails = new PreviewThumbnails(this);
}
} }
// Update the fullscreen support // Update the fullscreen support
+42 -12
View File
@@ -10,7 +10,7 @@ import { getElement, toggleClass } from './utils/elements';
import { ready, triggerEvent } from './utils/events'; import { ready, triggerEvent } from './utils/events';
import i18n from './utils/i18n'; import i18n from './utils/i18n';
import is from './utils/is'; import is from './utils/is';
import loadImage from './utils/loadImage'; import loadImage from './utils/load-image';
const ui = { const ui = {
addStyleHook() { addStyleHook() {
@@ -67,15 +67,15 @@ const ui = {
// Reset mute state // Reset mute state
this.muted = null; this.muted = null;
// Reset speed
this.speed = null;
// Reset loop state // Reset loop state
this.loop = null; this.loop = null;
// Reset quality setting // Reset quality setting
this.quality = null; this.quality = null;
// Reset speed
this.speed = null;
// Reset volume display // Reset volume display
controls.updateVolume.call(this); controls.updateVolume.call(this);
@@ -170,7 +170,7 @@ const ui = {
} }
// Set property synchronously to respect the call order // Set property synchronously to respect the call order
this.media.setAttribute('poster', poster); this.media.setAttribute('data-poster', poster);
// Wait until ui is ready // Wait until ui is ready
return ( return (
@@ -198,7 +198,9 @@ const ui = {
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '', backgroundSize: '',
}); });
ui.togglePoster.call(this, true); ui.togglePoster.call(this, true);
return poster; return poster;
}) })
); );
@@ -213,7 +215,8 @@ const ui = {
// Set state // Set state
Array.from(this.elements.buttons.play || []).forEach(target => { Array.from(this.elements.buttons.play || []).forEach(target => {
target.pressed = this.playing; Object.assign(target, { pressed: this.playing });
target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
}); });
// Only update controls on non timeupdate events // Only update controls on non timeupdate events
@@ -233,25 +236,52 @@ const ui = {
clearTimeout(this.timers.loading); clearTimeout(this.timers.loading);
// Timer to prevent flicker when seeking // Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => { this.timers.loading = setTimeout(
() => {
// Update progress bar loading class state // Update progress bar loading class state
toggleClass(this.elements.container, this.config.classNames.loading, this.loading); toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Update controls visibility // Update controls visibility
ui.toggleControls.call(this); ui.toggleControls.call(this);
}, this.loading ? 250 : 0); },
this.loading ? 250 : 0,
);
}, },
// Toggle controls based on state and `force` argument // Toggle controls based on state and `force` argument
toggleControls(force) { toggleControls(force) {
const { controls } = this.elements; const { controls: controlsElement } = this.elements;
if (controls && this.config.hideControls) { if (controlsElement && this.config.hideControls) {
// Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.) // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
const recentTouchSeek = (this.touch && this.lastSeekTime + 2000 > Date.now()); const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover || recentTouchSeek)); this.toggleControls(
Boolean(
force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,
),
);
}
},
// Migrate any custom properties from the media to the parent
migrateStyles() {
// Loop through values (as they are the keys when the object is spread 🤔)
Object.values({ ...this.media.style })
// We're only fussed about Plyr specific properties
.filter(key => !is.empty(key) && key.startsWith('--plyr'))
.forEach(key => {
// Set on the container
this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));
// Clean up from media element
this.media.style.removeProperty(key);
});
// Remove attribute if empty
if (is.empty(this.media.style)) {
this.media.removeAttribute('style');
} }
}, },
}; };
+10 -6
View File
@@ -2,7 +2,6 @@
// Animation utils // Animation utils
// ========================================================================== // ==========================================================================
import { toggleHidden } from './elements';
import is from './is'; import is from './is';
export const transitionEndEvent = (() => { export const transitionEndEvent = (() => {
@@ -21,14 +20,19 @@ export const transitionEndEvent = (() => {
})(); })();
// Force repaint of element // Force repaint of element
export function repaint(element) { export function repaint(element, delay) {
setTimeout(() => { setTimeout(() => {
try { try {
toggleHidden(element, true); // eslint-disable-next-line no-param-reassign
element.offsetHeight; // eslint-disable-line element.hidden = true;
toggleHidden(element, false);
// eslint-disable-next-line no-unused-expressions
element.offsetHeight;
// eslint-disable-next-line no-param-reassign
element.hidden = false;
} catch (e) { } catch (e) {
// Do nothing // Do nothing
} }
}, 0); }, delay);
} }
+33 -52
View File
@@ -2,8 +2,8 @@
// Element utils // Element utils
// ========================================================================== // ==========================================================================
import { toggleListener } from './events';
import is from './is'; import is from './is';
import { extend } from './objects';
// Wrap an element // Wrap an element
export function wrap(elements, wrapper) { export function wrap(elements, wrapper) {
@@ -16,7 +16,6 @@ export function wrap(elements, wrapper) {
.reverse() .reverse()
.forEach((element, index) => { .forEach((element, index) => {
const child = index > 0 ? wrapper.cloneNode(true) : wrapper; const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
// Cache the current parent and sibling. // Cache the current parent and sibling.
const parent = element.parentNode; const parent = element.parentNode;
const sibling = element.nextSibling; const sibling = element.nextSibling;
@@ -137,30 +136,28 @@ export function getAttributesFromSelector(sel, existingAttributes) {
} }
const attributes = {}; const attributes = {};
const existing = existingAttributes; const existing = extend({}, existingAttributes);
sel.split(',').forEach(s => { sel.split(',').forEach(s => {
// Remove whitespace // Remove whitespace
const selector = s.trim(); const selector = s.trim();
const className = selector.replace('.', ''); const className = selector.replace('.', '');
const stripped = selector.replace(/[[\]]/g, ''); const stripped = selector.replace(/[[\]]/g, '');
// Get the parts and value // Get the parts and value
const parts = stripped.split('='); const parts = stripped.split('=');
const key = parts[0]; const [key] = parts;
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
// Get the first character // Get the first character
const start = selector.charAt(0); const start = selector.charAt(0);
switch (start) { switch (start) {
case '.': case '.':
// Add to existing classname // Add to existing classname
if (is.object(existing) && is.string(existing.class)) { if (is.string(existing.class)) {
existing.class += ` ${className}`; attributes.class = `${existing.class} ${className}`;
} } else {
attributes.class = className; attributes.class = className;
}
break; break;
case '#': case '#':
@@ -179,7 +176,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {
} }
}); });
return attributes; return extend(existing, attributes);
} }
// Toggle hidden // Toggle hidden
@@ -194,11 +191,8 @@ export function toggleHidden(element, hidden) {
hide = !element.hidden; hide = !element.hidden;
} }
if (hide) { // eslint-disable-next-line no-param-reassign
element.setAttribute('hidden', ''); element.hidden = hide;
} else {
element.removeAttribute('hidden');
}
} }
// Mirror Element.classList.toggle, with IE compatibility for "force" argument // Mirror Element.classList.toggle, with IE compatibility for "force" argument
@@ -227,20 +221,40 @@ export function hasClass(element, className) {
// Element matches selector // Element matches selector
export function matches(element, selector) { export function matches(element, selector) {
const prototype = { Element }; const { prototype } = Element;
function match() { function match() {
return Array.from(document.querySelectorAll(selector)).includes(this); return Array.from(document.querySelectorAll(selector)).includes(this);
} }
const matches = const method =
prototype.matches || prototype.matches ||
prototype.webkitMatchesSelector || prototype.webkitMatchesSelector ||
prototype.mozMatchesSelector || prototype.mozMatchesSelector ||
prototype.msMatchesSelector || prototype.msMatchesSelector ||
match; match;
return matches.call(element, selector); return method.call(element, selector);
}
// Closest ancestor element matching selector (also tests element itself)
export function closest(element, selector) {
const { prototype } = Element;
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
function closestElement() {
let el = this;
do {
if (matches.matches(el, selector)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
}
const method = prototype.closest || closestElement;
return method.call(element, selector);
} }
// Find all elements // Find all elements
@@ -253,39 +267,6 @@ export function getElement(selector) {
return this.elements.container.querySelector(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 // Set focus and tab focus class
export function setFocus(element = null, tabFocus = false) { export function setFocus(element = null, tabFocus = false) {
if (!is.element(element)) { if (!is.element(element)) {
+1 -4
View File
@@ -35,7 +35,6 @@ export function toggleListener(element, event, callback, toggle = false, passive
// Allow multiple events // Allow multiple events
const events = event.split(' '); const events = event.split(' ');
// Build options // Build options
// Default to just the capture boolean for browsers with no passive listener support // Default to just the capture boolean for browsers with no passive listener support
let options = capture; let options = capture;
@@ -91,9 +90,7 @@ export function triggerEvent(element, type = '', bubbles = false, detail = {}) {
// Create and dispatch the event // Create and dispatch the event
const event = new CustomEvent(type, { const event = new CustomEvent(type, {
bubbles, bubbles,
detail: Object.assign({}, detail, { detail: { ...detail, plyr: this },
plyr: this,
}),
}); });
// Dispatch the event // Dispatch the event
+2 -2
View File
@@ -36,8 +36,8 @@ const i18n = {
'{title}': config.title, '{title}': config.title,
}; };
Object.entries(replace).forEach(([key, value]) => { Object.entries(replace).forEach(([k, v]) => {
string = replaceAll(string, key, value); string = replaceAll(string, k, v);
}); });
return string; return string;
+1 -1
View File
@@ -19,7 +19,7 @@ const isEvent = input => instanceOf(input, Event);
const isKeyboardEvent = input => instanceOf(input, KeyboardEvent); const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);
const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind)); const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));
const isPromise = input => instanceOf(input, Promise); const isPromise = input => instanceOf(input, Promise) && isFunction(input.then);
const isEmpty = input => const isEmpty = input =>
isNullOrUndefined(input) || isNullOrUndefined(input) ||
+75
View File
@@ -0,0 +1,75 @@
// ==========================================================================
// 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) => {
// eslint-disable-next-line no-param-reassign
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(() => {});
}
}
-76
View File
@@ -1,76 +0,0 @@
// ==========================================================================
// 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(() => {});
}
}
+17
View File
@@ -0,0 +1,17 @@
/**
* Returns a number whose value is limited to the given range.
*
* Example: limit the output of this computation to between 0 and 255
* (x * 255).clamp(0, 255)
*
* @param {Number} input
* @param {Number} min The lower boundary of the output range
* @param {Number} max The upper boundary of the output range
* @returns A number in the range [min, max]
* @type Number
*/
export function clamp(input = 0, min = 0, max = 255) {
return Math.min(Math.max(input, min), max);
}
export default { clamp };
+14
View File
@@ -0,0 +1,14 @@
import is from './is';
/**
* Silence a Promise-like object.
* This is useful for avoiding non-harmful, but potentially confusing "uncaught
* play promise" rejection error messages.
* @param {Object} value An object that may or may not be `Promise`-like.
*/
export function silencePromise(value) {
if (is.promise(value)) {
value.then(null, () => {});
}
}
export default { silencePromise };
+4 -9
View File
@@ -28,17 +28,12 @@ export function getPercentage(current, max) {
} }
// Replace all occurances of a string in a string // Replace all occurances of a string in a string
export function replaceAll(input = '', find = '', replace = '') { export const replaceAll = (input = '', find = '', replace = '') =>
return input.replace( input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'),
replace.toString(),
);
}
// Convert to title case // Convert to title case
export function toTitleCase(input = '') { export const toTitleCase = (input = '') =>
return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase()); input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
}
// Convert string to pascalCase // Convert string to pascalCase
export function toPascalCase(input = '') { export function toPascalCase(input = '') {
+55 -16
View File
@@ -4,34 +4,73 @@
import is from './is'; import is from './is';
/* function reduceAspectRatio(width, height) { export function validateRatio(input) {
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h)); if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {
const ratio = getRatio(width, height); return false;
return `${width / ratio}:${height / ratio}`; }
} */
// Set aspect ratio for responsive container const ratio = is.array(input) ? input : input.split(':');
export function setAspectRatio(input) {
let ratio = input;
if (!is.string(ratio) && !is.nullOrUndefined(this.embed)) { return ratio.map(Number).every(is.number);
}
export function reduceAspectRatio(ratio) {
if (!is.array(ratio) || !ratio.every(is.number)) {
return null;
}
const [width, height] = ratio;
const getDivider = (w, h) => (h === 0 ? w : getDivider(h, w % h));
const divider = getDivider(width, height);
return [width / divider, height / divider];
}
export function getAspectRatio(input) {
const parse = ratio => (validateRatio(ratio) ? ratio.split(':').map(Number) : null);
// Try provided ratio
let ratio = parse(input);
// Get from config
if (ratio === null) {
ratio = parse(this.config.ratio);
}
// Get from embed
if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {
({ ratio } = this.embed); ({ ratio } = this.embed);
} }
if (!is.string(ratio)) { // Get from HTML5 video
({ ratio } = this.config); if (ratio === null && this.isHTML5) {
const { videoWidth, videoHeight } = this.media;
ratio = reduceAspectRatio([videoWidth, videoHeight]);
} }
const [x, y] = ratio.split(':').map(Number); return ratio;
const padding = (100 / x) * y; }
this.elements.wrapper.style.paddingBottom = `${padding}%`; // Set aspect ratio for responsive container
export function setAspectRatio(input) {
if (!this.isVideo) {
return {};
}
const { wrapper } = this.elements;
const ratio = getAspectRatio.call(this, input);
const [w, h] = is.array(ratio) ? ratio : [0, 0];
const padding = (100 / w) * h;
wrapper.style.paddingBottom = `${padding}%`;
// For Vimeo we have an extra <div> to hide the standard controls and UI // For Vimeo we have an extra <div> to hide the standard controls and UI
if (this.isVimeo && this.supported.ui) { if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {
const height = 240; const height = (100 / this.media.offsetWidth) * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);
const offset = (height - padding) / (height / 50); const offset = (height - padding) / (height / 50);
this.media.style.transform = `translateY(-${offset}%)`; this.media.style.transform = `translateY(-${offset}%)`;
} else if (this.isHTML5) {
wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null);
} }
return { padding, ratio }; return { padding, ratio };
+1 -2
View File
@@ -13,12 +13,11 @@ export const getSeconds = value => Math.trunc(value % 60, 10);
export function formatTime(time = 0, displayHours = false, inverted = false) { export function formatTime(time = 0, displayHours = false, inverted = false) {
// Bail if the value isn't a number // Bail if the value isn't a number
if (!is.number(time)) { if (!is.number(time)) {
return formatTime(null, displayHours, inverted); return formatTime(undefined, displayHours, inverted);
} }
// Format time component to add leading zero // Format time component to add leading zero
const format = value => `0${value}`.slice(-2); const format = value => `0${value}`.slice(-2);
// Breakdown to hours, mins, secs // Breakdown to hours, mins, secs
let hours = getHours(time); let hours = getHours(time);
const mins = getMinutes(time); const mins = getMinutes(time);
+9 -5
View File
@@ -5,24 +5,28 @@
// Base // Base
.plyr { .plyr {
@include plyr-font-smoothing($plyr-font-smoothing); @include plyr-font-smoothing($plyr-font-smoothing);
align-items: center;
direction: ltr; direction: ltr;
display: flex;
flex-direction: column;
font-family: $plyr-font-family; font-family: $plyr-font-family;
font-variant-numeric: tabular-nums; // Force monosace-esque number widths font-variant-numeric: tabular-nums; // Force monosace-esque number widths
font-weight: $plyr-font-weight-regular; font-weight: $plyr-font-weight-regular;
height: 100%;
line-height: $plyr-line-height; line-height: $plyr-line-height;
max-width: 100%; max-width: 100%;
min-width: 200px; min-width: 200px;
position: relative; position: relative;
text-shadow: none; text-shadow: none;
transition: box-shadow 0.3s ease; transition: box-shadow 0.3s ease;
z-index: 0; // Force any border radius
// Media elements // Media elements
video, video,
audio { audio,
border-radius: inherit; iframe {
height: auto; display: block;
vertical-align: middle; height: 100%;
width: 100%; width: 100%;
} }
+3 -3
View File
@@ -3,9 +3,9 @@
// -------------------------------------------------------------- // --------------------------------------------------------------
.plyr__badge { .plyr__badge {
background: $plyr-badge-bg; background: $plyr-badge-background;
border-radius: 2px; border-radius: $plyr-badge-border-radius;
color: $plyr-badge-color; color: $plyr-badge-text-color;
font-size: $plyr-font-size-badge; font-size: $plyr-font-size-badge;
line-height: 1; line-height: 1;
padding: 3px 4px; padding: 3px 4px;
+16 -17
View File
@@ -10,7 +10,6 @@
.plyr__captions { .plyr__captions {
animation: plyr-fade-in 0.3s ease; animation: plyr-fade-in 0.3s ease;
bottom: 0; bottom: 0;
color: $plyr-captions-color;
display: none; display: none;
font-size: $plyr-font-size-captions-small; font-size: $plyr-font-size-captions-small;
left: 0; left: 0;
@@ -20,27 +19,13 @@
transition: transform 0.4s ease-in-out; transition: transform 0.4s ease-in-out;
width: 100%; width: 100%;
.plyr__caption {
background: $plyr-captions-bg;
border-radius: 2px;
box-decoration-break: clone;
line-height: 185%;
padding: 0.2em 0.5em;
white-space: pre-wrap;
// Firefox adds a <div> when using getCueAsHTML()
div {
display: inline;
}
}
span:empty { span:empty {
display: none; display: none;
} }
@media (min-width: $plyr-bp-sm) { @media (min-width: $plyr-bp-sm) {
font-size: $plyr-font-size-captions-base; font-size: $plyr-font-size-captions-base;
padding: ($plyr-control-spacing * 2); padding: calc(#{$plyr-control-spacing} * 2);
} }
@media (min-width: $plyr-bp-md) { @media (min-width: $plyr-bp-md) {
@@ -54,6 +39,20 @@
// If the lower controls are shown and not empty // If the lower controls are shown and not empty
.plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty) ~ .plyr__captions { .plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty) ~ .plyr__captions {
transform: translateY(-($plyr-control-spacing * 4)); transform: translateY(calc(#{$plyr-control-spacing} * -4));
} }
.plyr__caption {
background: $plyr-captions-background;
border-radius: 2px;
box-decoration-break: clone;
color: $plyr-captions-text-color;
line-height: 185%;
padding: 0.2em 0.5em;
white-space: pre-wrap;
// Firefox adds a <div> when using getCueAsHTML()
div {
display: inline;
}
}
-61
View File
@@ -50,64 +50,3 @@ a.plyr__control {
.plyr__control.plyr__control--pressed .label--not-pressed { .plyr__control.plyr__control--pressed .label--not-pressed {
display: none; display: none;
} }
// Audio control
.plyr--audio .plyr__control {
&.plyr__tab-focus,
&:hover,
&[aria-expanded='true'] {
background: $plyr-audio-control-bg-hover;
color: $plyr-audio-control-color-hover;
}
}
// 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);
border: 0;
border-radius: 100%;
box-shadow: 0 1px 1px rgba(#000, 0.15);
color: $plyr-video-control-color;
display: none;
left: 50%;
padding: ceil($plyr-control-spacing * 1.5);
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
// Offset icon to make the play button look right
svg {
left: 2px;
position: relative;
}
&:hover,
&:focus {
background: $plyr-video-control-bg-hover;
}
}
.plyr--playing .plyr__control--overlaid {
opacity: 0;
visibility: hidden;
}
.plyr--full-ui.plyr--video .plyr__control--overlaid {
display: block;
}
+23 -63
View File
@@ -14,79 +14,39 @@
justify-content: flex-end; justify-content: flex-end;
text-align: center; text-align: center;
.plyr__progress__container {
flex: 1;
min-width: 0; // Fix for Edge issue where content would overflow
}
// Spacing // Spacing
> .plyr__control, .plyr__controls__item {
.plyr__progress, margin-left: calc(#{$plyr-control-spacing} / 4);
.plyr__time,
.plyr__menu,
.plyr__volume {
margin-left: ($plyr-control-spacing / 2);
}
.plyr__menu + .plyr__control, &:first-child {
> .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-left: 0;
margin-right: auto; margin-right: auto;
} }
&.plyr__progress__container {
padding-left: calc(#{$plyr-control-spacing} / 4);
}
&.plyr__time {
padding: 0 calc(#{$plyr-control-spacing} / 2);
}
&.plyr__progress__container:first-child,
&.plyr__time:first-child,
&.plyr__time + .plyr__time {
padding-left: 0;
}
}
// Hide empty controls // Hide empty controls
&:empty { &:empty {
display: none; display: none;
} }
@media (min-width: $plyr-bp-sm) {
> .plyr__control,
.plyr__menu,
.plyr__progress,
.plyr__time,
.plyr__volume {
margin-left: $plyr-control-spacing;
}
}
}
// Audio controls
.plyr--audio .plyr__controls {
background: $plyr-audio-controls-bg;
border-radius: inherit;
color: $plyr-audio-control-color;
padding: $plyr-control-spacing;
}
// 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;
transform: translateY(100%);
} }
// Some options are hidden by default // Some options are hidden by default
-36
View File
@@ -1,36 +0,0 @@
// --------------------------------------------------------------
// Embedded players
// YouTube, Vimeo, etc
// --------------------------------------------------------------
// Default to 16:9 ratio but this is set by JavaScript based on config
$embed-padding: ((100 / 16) * 9);
.plyr__video-embed {
height: 0;
padding-bottom: to-percentage($embed-padding);
position: relative;
iframe {
border: 0;
height: 100%;
left: 0;
position: absolute;
top: 0;
user-select: none;
width: 100%;
}
}
// If the full custom UI is supported
.plyr--full-ui .plyr__video-embed {
$height: 240;
$offset: to-percentage(($height - $embed-padding) / ($height / 50));
// Only used for Vimeo
> .plyr__video-embed__container {
padding-bottom: to-percentage($height);
position: relative;
transform: translateY(-$offset);
}
}

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