Compare commits

...

563 Commits

Author SHA1 Message Date
32444c9851 v3.6.7
- Fix: remove regression caused by optional chaining and nullish coalescing in check for `window.CSS` check for aspect-ratio (fixes #2174)
2021-04-20 08:28:23 +10:00
b93dcc43ad fix: minor syntax tweak 2021-04-19 21:16:30 +10:00
6ff6ff1673 fix: syntax fix (#2176) 2021-04-19 21:13:06 +10:00
e1ff86820c v3.6.6
- Improvements to how aspect ratio is handled. Use `aspect-ratio` CSS property instead of the legacy method (still used as fallback). Also automatically determined aspect ratios are rounded to the nearast standard ratio. This fixes issues with the YouTube embeds showing a 1-2px black bar.
- Hide the YouTube poster image container when paused so that the controls underneath can be used.
2021-04-18 17:49:23 +10:00
7d22c361d1 chore: remove fastly purging logic (#2173) 2021-04-18 17:41:58 +10:00
951cccb6b0 fix: youtube poster allow clickthrough while paused (#2172) 2021-04-18 16:59:00 +10:00
438e425838 fix: aspect ratio improvements (#2171)
- Use CSS aspect-ratio (retain fallback for legacy browsers)
- Round aspect ratios (fixes YouTube black border issue)
2021-04-18 16:58:44 +10:00
ddb5ad071e chore: deploy 2021-04-17 00:30:30 +10:00
38060d986c chore: release 2021-04-17 00:27:07 +10:00
8c74396459 chore: linting 2021-04-17 00:27:07 +10:00
a09d63e8f8 chore(deps): bump is-svg from 4.2.1 to 4.3.1 (#2148)
Bumps [is-svg](https://github.com/sindresorhus/is-svg) from 4.2.1 to 4.3.1.
- [Release notes](https://github.com/sindresorhus/is-svg/releases)
- [Commits](https://github.com/sindresorhus/is-svg/compare/v4.2.1...v4.3.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-17 00:23:18 +10:00
d74af9a73d fix: regression regarding poster image 2021-04-17 00:19:57 +10:00
60310693d3 chore: packge updates, switch to cssnano 2021-04-17 00:19:41 +10:00
5d08821c9b fix: missing styles for embeds 2021-04-17 00:19:05 +10:00
3316e40e7b fix: issue with IE detection 2021-04-17 00:17:53 +10:00
dfe5985326 added Runway360 to list of sites using plyr (#2117) 2021-03-27 19:48:11 +11:00
b27ad17c59 Migrate color formatting to colorette (#2124) 2021-03-27 19:46:41 +11:00
14b8b24e14 Merge pull request #2152 from didrip/fix-vimeo-video-play-chrome-89
add all required props to vimeo iframe allow attribute - fixes #2151
2021-03-26 20:50:53 +11:00
711fc5835f add all required props to vimeo iframe allow attribute - fixes #2151 2021-03-24 14:06:50 +01:00
028be22dec chore: removed dist directories from git 2021-01-30 00:06:48 +11:00
3b5dc09285 Merge pull request #2093 from sampotts/develop
Develop
2021-01-29 23:54:46 +11:00
8596e05df3 Merge branch 'master' into develop
# Conflicts:
#	src/js/utils/is.js
2021-01-29 23:43:10 +11:00
1aee8f53a7 chore: publish 3.6.4 2021-01-29 23:40:34 +11:00
a187d07807 chore: linting fix 2021-01-29 23:28:02 +11:00
3096dd9513 Update pull_request_template.md 2021-01-29 23:18:47 +11:00
150b07f3ef Update .travis.yml 2021-01-29 23:17:08 +11:00
ad7778e78d Delete .travis directory 2021-01-29 23:16:52 +11:00
c8e776bbea Delete prevent-base-master.sh 2021-01-29 23:16:33 +11:00
c66dc8bf95 Update .travis.yml 2021-01-29 23:16:15 +11:00
ea87b9c788 Update CONTRIBUTING.md 2021-01-29 23:15:44 +11:00
aa3378fd73 v3.6.4 (#2089)
* force fullscreen events to trigger on plyr element (media element in iOS) and not fullscreen container

* Fixing "missing code in detail" for PlyrEvent type

When using typescript and listening for youtube statechange event, it is missing the code property definition inside the event (even though it is provided in the code).
By making events a map of key-value, we can add easily custom event type for specific event name. Since YouTube "statechange" event differs from the basic PlyrEvent, I added a new Event Type "PlyrStateChangeEvent" having a code property corresponding to a YoutubeState enum defined by the YouTube API documentation.
This pattern follows how addEventListener in the lib.dom.d.ts is defined.

* Update link to working dash.js demo (was broken)

* Fix PreviewThumbnailsOptions type

According to the docs, the `src` should also accept an array of strings.

* fix issue #1872

* Check if key is a string before attempt --plyr checking

* Fix for Slow loading videos not autoplaying

* Fix for Slow loading videos not autoplaying

* Network requests are not cancelled after the player is destroyed

* Fix for apect ratio problem when using Vimeo player on mobile devices (issue #1940)

* chore: update packages and linting

* Invoke custom listener on triggering fullscreen via double-click

* Fix volume when unmuting from volume 0

* adding a nice Svelte plugin that I found

* Add missing unit to calc in media query

* Assigning player's lastSeekTime on rewind/fast forward to prevent immediate controls hide on mobile

* Fix youtube not working when player is inside shadow dom

* v3.6.2

* ESLint to use common config

* add BitChute to users list

* Fix aspect ratio issue

* Revert noCookie change

* feat: demo radius tweaks

* fix: poster image shouldn’t receive click events

* chore: package updates

* chore: linting

* feat: custom controls option for embedded players

* Package upgrades

* ESLint to use common config

* Linting changes

* Update README.md

* chore: formatting

* fix: revert pointer events change for poster

* fix: hack for Safari 14 not repainting Vimeo embed on entering fullscreen

* fix: demo using custom controls for YouTube

* doc: Add STROLLÿN among the list of Plyr users

* Fixes #2005

* fix: overflowing volume slider

* chore: clean up CSS

* fix: hide poster when not using custom controls

* Package upgrades

* ESLint to use common config

* Linting changes

* chore: revert customControls default option (to prevent breaking change)

* docs: changelog for v3.6.3

* Remove unnecessary calc from media query (#2049)

* Enhance types (#1841)

* 🏷️(type) enhance QualityOptions type

Some optional properties in the QualityOptions were missing. The forced
and onChange property allwoing to use an external handler.

* ♻️(type) use Plyr.Provider for the readonly provider property

A type exists to define all available providers. This type isn't used in
the Plyr class definition and the same provider list is also defined.
This code is refactored to use the Plyr.Provider type

* 🏷️(type) add missing elements property in Plyr class

In Plyr class, you can access elements set in cache. This property is
missing in the class definition. The Plyr.Elements is for now
incomplete.

* FIX - object values for the providers must be used (#2053)

To make use of the provider configuration, the objects values must be used.

* Fix to work inside iframes. (#2069)

* Fix to work inside iframes.

Right now Plyr fails to load inside iframes because the selectors are not instances of Element (iframes have their own, separate globals). This is an alternative method to check isElement that will work inside iframes. This is battle-tested fallback code used before browsers supported HTMLElement.

* Update is.js

* Added --plyr-video-background for having control over the background of a video with alpha channel (webm) or a poster image with alpha channel. (#2076)

* Fix issue with not entering iosfullscreen of vimeo videos with playsi… (#2038)

* Fix issue with not entering iosfullscreen of vimeo videos with playsinline=true

* Use isVimeo-function instead of hardcoded value

Co-authored-by: Julian Frosch <julian.frosch@gmail.com>

* fix: use new syntax for iframe allow attribute

* Bump ini from 1.3.5 to 1.3.7 (#2044)

Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore: package updates

* chore: add @babel/plugin-proposal-class-properties

* fix: use bound arrow functions in classes

* chore: cleanup commented out code

* chore: release

Co-authored-by: Som Meaden <som@theprojectsomething.com>
Co-authored-by: akuma06 <demon.akuma06@gmail.com>
Co-authored-by: Jonathan Arbely <dev@jonathanarbely.de>
Co-authored-by: Takeshi <iwatakeshi@users.noreply.github.com>
Co-authored-by: Hex <hex@codeigniter.org.cn>
Co-authored-by: Syed Husain <syed.husain@appspace.com>
Co-authored-by: Danielh112 <Daniel@sbgsportssoftware.com>
Co-authored-by: Danil Stoyanov <d.stoyanov@corp.mail.ru>
Co-authored-by: Guru Prasad Srinivasa <gurupras@buffalo.edu>
Co-authored-by: Stephane Fortin Bouchard <stephane.f.bouchard@gmail.com>
Co-authored-by: Zev Averbach <zev@averba.ch>
Co-authored-by: Vincent Orback <hello@vincentorback.se>
Co-authored-by: trafium <trafium@gmail.com>
Co-authored-by: xansen <27698939+xansen@users.noreply.github.com>
Co-authored-by: zoomerdev <59863739+zoomerdev@users.noreply.github.com>
Co-authored-by: Mikaël Castellani <mikael.castellani@gmail.com>
Co-authored-by: dirkjf <d.j.faber@outlook.com>
Co-authored-by: Naomi <naomizuiverloon@gmail.com>
Co-authored-by: Manuel Raynaud <manu@raynaud.io>
Co-authored-by: syteknet-core <github.core@sytek.net>
Co-authored-by: Andre Gagnon <ajgagnon@uwalumni.com>
Co-authored-by: Nepomuk Leutschacher <864660+nepomuc@users.noreply.github.com>
Co-authored-by: Elias Saalmann <lordon@users.noreply.github.com>
Co-authored-by: Julian Frosch <julian.frosch@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-01-29 23:14:02 +11:00
2cebf9b8a3 chore: release 2021-01-29 23:07:14 +11:00
572aeacf5b chore: cleanup commented out code 2021-01-29 23:06:57 +11:00
b5592b0fb5 fix: use bound arrow functions in classes 2021-01-29 22:47:27 +11:00
9adf35a444 chore: add @babel/plugin-proposal-class-properties 2021-01-29 22:47:27 +11:00
39ff31e3e3 chore: package updates 2021-01-29 22:47:27 +11:00
7e82ec6d70 Bump ini from 1.3.5 to 1.3.7 (#2044)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-01-29 22:47:27 +11:00
f4d6a243cd fix: use new syntax for iframe allow attribute 2021-01-29 22:47:27 +11:00
31b5027b1e Fix issue with not entering iosfullscreen of vimeo videos with playsi… (#2038)
* Fix issue with not entering iosfullscreen of vimeo videos with playsinline=true

* Use isVimeo-function instead of hardcoded value

Co-authored-by: Julian Frosch <julian.frosch@gmail.com>
2021-01-29 22:38:26 +11:00
7750314058 Added --plyr-video-background for having control over the background of a video with alpha channel (webm) or a poster image with alpha channel. (#2076) 2021-01-24 16:43:36 +11:00
16c134fc1e Fix to work inside iframes. (#2069)
* Fix to work inside iframes.

Right now Plyr fails to load inside iframes because the selectors are not instances of Element (iframes have their own, separate globals). This is an alternative method to check isElement that will work inside iframes. This is battle-tested fallback code used before browsers supported HTMLElement.

* Update is.js
2021-01-24 10:32:45 +11:00
f00e81a976 FIX - object values for the providers must be used (#2053)
To make use of the provider configuration, the objects values must be used.
2020-12-23 09:30:23 +11:00
32a4039f7c Enhance types (#1841)
* 🏷️(type) enhance QualityOptions type

Some optional properties in the QualityOptions were missing. The forced
and onChange property allwoing to use an external handler.

* ♻️(type) use Plyr.Provider for the readonly provider property

A type exists to define all available providers. This type isn't used in
the Plyr class definition and the same provider list is also defined.
This code is refactored to use the Plyr.Provider type

* 🏷️(type) add missing elements property in Plyr class

In Plyr class, you can access elements set in cache. This property is
missing in the class definition. The Plyr.Elements is for now
incomplete.
2020-12-20 20:18:53 +11:00
ba09bc32d3 fix: use bound arrow functions in classes 2020-12-20 20:12:21 +11:00
d7195d5276 chore: add @babel/plugin-proposal-class-properties 2020-12-20 20:12:21 +11:00
6a6fb8d41a chore: package updates 2020-12-20 20:12:21 +11:00
c853adc760 Remove unnecessary calc from media query (#2049) 2020-12-20 19:10:50 +11:00
ca041f2e0d Bump ini from 1.3.5 to 1.3.7 (#2044)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-20 19:05:31 +11:00
87078d7a44 Merge branch 'master' into develop
# Conflicts:
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.min.mjs
#	dist/plyr.min.mjs.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	dist/plyr.polyfilled.min.mjs
#	dist/plyr.polyfilled.min.mjs.map
#	yarn.lock
2020-11-14 13:35:22 +11:00
c300df5742 chore: fix node version 2020-11-14 13:34:29 +11:00
2ba56fda15 Bump bl from 1.2.2 to 1.2.3 (#2017)
Bumps [bl](https://github.com/rvagg/bl) from 1.2.2 to 1.2.3.
- [Release notes](https://github.com/rvagg/bl/releases)
- [Commits](https://github.com/rvagg/bl/compare/v1.2.2...v1.2.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-11-14 13:34:04 +11:00
b444aa3a98 pkg: deploy v3.6.3 2020-11-14 13:29:47 +11:00
e8d883edba v3.6.3 (#2016)
* force fullscreen events to trigger on plyr element (media element in iOS) and not fullscreen container

* Fixing "missing code in detail" for PlyrEvent type

When using typescript and listening for youtube statechange event, it is missing the code property definition inside the event (even though it is provided in the code).
By making events a map of key-value, we can add easily custom event type for specific event name. Since YouTube "statechange" event differs from the basic PlyrEvent, I added a new Event Type "PlyrStateChangeEvent" having a code property corresponding to a YoutubeState enum defined by the YouTube API documentation.
This pattern follows how addEventListener in the lib.dom.d.ts is defined.

* Update link to working dash.js demo (was broken)

* Fix PreviewThumbnailsOptions type

According to the docs, the `src` should also accept an array of strings.

* fix issue #1872

* Check if key is a string before attempt --plyr checking

* Fix for Slow loading videos not autoplaying

* Fix for Slow loading videos not autoplaying

* Network requests are not cancelled after the player is destroyed

* Fix for apect ratio problem when using Vimeo player on mobile devices (issue #1940)

* chore: update packages and linting

* Invoke custom listener on triggering fullscreen via double-click

* Fix volume when unmuting from volume 0

* adding a nice Svelte plugin that I found

* Add missing unit to calc in media query

* Assigning player's lastSeekTime on rewind/fast forward to prevent immediate controls hide on mobile

* Fix youtube not working when player is inside shadow dom

* v3.6.2

* ESLint to use common config

* add BitChute to users list

* Fix aspect ratio issue

* Revert noCookie change

* feat: demo radius tweaks

* fix: poster image shouldn’t receive click events

* chore: package updates

* chore: linting

* feat: custom controls option for embedded players

* Package upgrades

* ESLint to use common config

* Linting changes

* Update README.md

* chore: formatting

* fix: revert pointer events change for poster

* fix: hack for Safari 14 not repainting Vimeo embed on entering fullscreen

* fix: demo using custom controls for YouTube

* doc: Add STROLLÿN among the list of Plyr users

* Fixes #2005

* fix: overflowing volume slider

* chore: clean up CSS

* fix: hide poster when not using custom controls

* Package upgrades

* ESLint to use common config

* Linting changes

* chore: revert customControls default option (to prevent breaking change)

* docs: changelog for v3.6.3

Co-authored-by: Som Meaden <som@theprojectsomething.com>
Co-authored-by: akuma06 <demon.akuma06@gmail.com>
Co-authored-by: Jonathan Arbely <dev@jonathanarbely.de>
Co-authored-by: Takeshi <iwatakeshi@users.noreply.github.com>
Co-authored-by: Hex <hex@codeigniter.org.cn>
Co-authored-by: Syed Husain <syed.husain@appspace.com>
Co-authored-by: Danielh112 <Daniel@sbgsportssoftware.com>
Co-authored-by: Danil Stoyanov <d.stoyanov@corp.mail.ru>
Co-authored-by: Guru Prasad Srinivasa <gurupras@buffalo.edu>
Co-authored-by: Stephane Fortin Bouchard <stephane.f.bouchard@gmail.com>
Co-authored-by: Zev Averbach <zev@averba.ch>
Co-authored-by: Vincent Orback <hello@vincentorback.se>
Co-authored-by: trafium <trafium@gmail.com>
Co-authored-by: xansen <27698939+xansen@users.noreply.github.com>
Co-authored-by: zoomerdev <59863739+zoomerdev@users.noreply.github.com>
Co-authored-by: Mikaël Castellani <mikael.castellani@gmail.com>
Co-authored-by: dirkjf <d.j.faber@outlook.com>
2020-11-14 13:24:11 +11:00
36b53d2cf0 docs: changelog for v3.6.3 2020-11-14 13:22:24 +11:00
2d4686a5f3 chore: revert customControls default option (to prevent breaking change) 2020-11-14 13:22:05 +11:00
31861bd475 Merge branch 'master' into develop
# Conflicts:
#	package.json
#	src/js/plugins/vimeo.js
#	src/js/plugins/youtube.js
#	src/js/ui.js
#	yarn.lock
2020-11-14 13:11:24 +11:00
c3e163741f Linting changes 2020-11-14 13:01:36 +11:00
1c715bc889 ESLint to use common config 2020-11-14 12:55:06 +11:00
35ef42246b Package upgrades 2020-11-14 12:55:06 +11:00
cd3962ca32 fix: hide poster when not using custom controls 2020-11-14 12:50:49 +11:00
da16c55427 chore: clean up CSS 2020-11-14 12:50:35 +11:00
bd8bacb6d5 fix: overflowing volume slider 2020-11-14 00:22:40 +11:00
0d393a5c92 Merge pull request #2008 from dirkjf/firefox-volume-fix
Fixes #2005
2020-11-14 00:22:18 +11:00
136bb525a5 Fixes #2005 2020-11-09 16:33:28 +01:00
e85737d855 Merge pull request #1992 from micaste/patch-2
doc: Add STROLLÿN among the list of Plyr users
2020-11-05 10:03:34 +11:00
8882317080 doc: Add STROLLÿN among the list of Plyr users 2020-10-25 07:39:43 +01:00
5c02205a4f fix: demo using custom controls for YouTube 2020-10-19 23:10:00 +11:00
45f7e20233 fix: hack for Safari 14 not repainting Vimeo embed on entering fullscreen 2020-10-19 23:09:46 +11:00
b116e62f37 fix: revert pointer events change for poster 2020-10-19 23:09:18 +11:00
fa6282d0e2 chore: formatting 2020-10-19 22:43:53 +11:00
80bdf04fde Merge pull request #1963 from gurupras/dblclick-fullscreen-custom-listener
Invoke custom listener on double-clicking on video (fullscreen)
2020-10-19 22:41:34 +11:00
5d2c288721 Merge pull request #1961 from Benny739/master
fixed #1181 vimeo oembed api
2020-10-19 22:41:09 +11:00
3caa37fdf2 Merge pull request #1867 from jonathanarbely/patch-1
Update link to working dash.js demo (was broken)
2020-10-19 22:39:42 +11:00
807efcbc46 Update README.md 2020-10-19 22:39:31 +11:00
03d1e3d3e8 Merge pull request #1972 from zevaverbach/patch-2
adding a nice Svelte plugin that I found
2020-10-19 22:36:06 +11:00
664e2b4893 Merge pull request #1974 from vincentorback/patch-1
Add missing unit to calc in media query
2020-10-19 22:35:51 +11:00
4cf36bf203 Merge pull request #1967 from airtimemedia/1934-Youtube-unmute-volume-0
Fix volume when unmuting from volume 0
2020-10-19 22:35:12 +11:00
92f3728d80 Merge pull request #1977 from trafium/prevent-mobile-control-hiding-on-rewind-and-fast-forward
Prevent immediate hiding of controls on mobile when using rewind and fast forward
2020-10-19 22:34:30 +11:00
29d3ae0b67 Merge pull request #1984 from xansen/develop
Fix youtube not working when player is inside shadow dom
2020-10-19 22:33:52 +11:00
df91f7faac Merge branch 'master' into develop
# Conflicts:
#	README.md
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.min.mjs
#	dist/plyr.min.mjs.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	dist/plyr.polyfilled.min.mjs
#	dist/plyr.polyfilled.min.mjs.map
#	package.json
#	src/js/plugins/vimeo.js
#	src/js/plugins/youtube.js
#	src/js/ui.js
#	yarn.lock
2020-10-19 22:31:31 +11:00
9a69ae2599 Linting changes 2020-10-19 22:27:16 +11:00
02321c35bc ESLint to use common config 2020-10-19 22:27:16 +11:00
7c6316d824 Package upgrades 2020-10-19 22:27:16 +11:00
776b0c4036 feat: custom controls option for embedded players 2020-10-19 22:26:33 +11:00
fa653a8859 chore: linting 2020-10-19 22:25:46 +11:00
b6b7db7327 chore: package updates 2020-10-19 22:25:10 +11:00
d4b4303b8e fix: poster image shouldn’t receive click events 2020-10-19 21:43:20 +11:00
102fb1a32f feat: demo radius tweaks 2020-10-19 21:43:20 +11:00
c77ba3ecf0 Revert noCookie change 2020-10-19 21:43:20 +11:00
760f5f9469 Fix aspect ratio issue 2020-10-19 21:43:20 +11:00
0135e9c7f4 add BitChute to users list 2020-10-19 21:43:20 +11:00
2426c256cc ESLint to use common config 2020-10-19 21:43:05 +11:00
93eed08d14 v3.6.2 2020-10-19 21:42:19 +11:00
5a164780d8 Fix youtube not working when player is inside shadow dom 2020-10-19 01:38:42 +06:00
30989e4dbc Merge pull request #1982 from tbntdima/feat/fix-fulscreen-type
fix(plyr.d.ts): add force to FullScreenOptions
2020-10-15 09:09:49 +11:00
967206c695 fix(plyr.d.ts): add force to FullScreenOptions 2020-10-14 17:09:14 +02:00
272d39c1d1 Assigning player's lastSeekTime on rewind/fast forward to prevent immediate controls hide on mobile 2020-10-12 10:28:57 +03:00
828abc8308 Add missing unit to calc in media query 2020-10-08 12:03:25 +02:00
6455a6acc0 adding a nice Svelte plugin that I found 2020-10-02 10:35:29 +02:00
51cb03dc32 Fix volume when unmuting from volume 0 2020-09-28 21:39:02 -04:00
4d25a33cb0 Invoke custom listener on triggering fullscreen via double-click 2020-09-23 18:24:53 -04:00
e646207ed6 vimeo oembed api 2020-09-23 01:24:11 +02:00
18b3f23c69 chore: update packages and linting 2020-08-30 16:10:25 +10:00
98dbb2e43f Merge pull request #1885 from mercuryseries/patch-1
Fix small typo
2020-08-30 15:52:19 +10:00
11e48b0181 Merge pull request #1824 from akuma06/patch-2
Fixing "missing code in detail" for PlyrEvent type
2020-08-30 15:48:50 +10:00
3a07669e30 Merge pull request #1916 from syedhusain-appspace/develop
Check if key is a string before attempt --plyr checking
2020-08-30 15:47:25 +10:00
3e70f32555 Merge pull request #1904 from sampotts/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-08-30 15:47:00 +10:00
b3d390076b Merge pull request #1914 from hex-ci/develop
fix issue #1872
2020-08-30 15:46:35 +10:00
80fbeeb415 Merge pull request #1920 from Kithaitaa/Kithaitaa-patch-2
Please add our website to the "Used by" list
2020-08-30 15:46:11 +10:00
8261ebd502 Merge pull request #1928 from ethanve/patch-1
fix: blankVideo type
2020-08-30 15:45:57 +10:00
f75820fbd3 Merge pull request #1931 from SBGSports/Autoplay-issue
Fix for Slow loading videos not autoplaying
2020-08-30 15:45:24 +10:00
8b0d84a9a4 Merge pull request #1935 from SBGSports/Cancel-network-requests-once-player-is-destroyed
Network requests are not cancelled after the player is destroyed
2020-08-30 15:44:55 +10:00
d582e43413 Merge pull request #1941 from danilstoyanov/vimeo-aspect-ratio-mobile
Fix for apect ratio problem when using Vimeo player on mobile devices…
2020-08-30 15:44:19 +10:00
75082bc73f Fix for apect ratio problem when using Vimeo player on mobile devices (issue #1940) 2020-08-28 23:07:12 +03:00
f7e9ee56d2 Fix merge conflicts 2020-08-18 11:29:25 +01:00
22af7f16ea Network requests are not cancelled after the player is destroyed 2020-08-18 11:15:58 +01:00
9076d054b9 Fix for Slow loading videos not autoplaying 2020-08-14 10:34:48 +01:00
4eaa1a72b5 Fix for Slow loading videos not autoplaying 2020-08-14 10:29:46 +01:00
6f77e21e51 chore: fix blankVideo types 2020-08-12 10:11:45 -04:00
7910fea011 Please add our website to the "Used by" list
I would be happy if you could add our website to the "used by" list. We are a video game magazine that has been using Plyr for some time now for. Thx in advance.

Besides that i send you a donation through PayPal a few days ago =)
2020-08-05 10:44:41 +02:00
4c1ae8f3ce Check if key is a string before attempt --plyr checking 2020-07-30 19:43:04 +08:00
Hex
0733133510 fix issue #1872 2020-07-29 17:01:21 +08:00
8e0cde2949 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-16 18:00:08 +00:00
423b7b276f Merge pull request #1900 from iwatakeshi/patch-1
Fix PreviewThumbnailsOptions type
2020-07-14 09:03:36 +10:00
cbd4a7cef4 Fix PreviewThumbnailsOptions type
According to the docs, the `src` should also accept an array of strings.
2020-07-13 13:00:16 -06:00
391afe2814 Merge pull request #1889 from rutheneum-bote/patch-1
Add us to the "Used by" list
2020-06-30 22:08:25 +10:00
68e3944bb8 Add us to the "Used by" list
We're the students' newspaper of the Goethe Grammar School/Rutheneum since 1608 and we use Plyr in order to present the students' songs that were submitted to our school's creativity contest.
2020-06-30 11:47:09 +00:00
2ee6ae16b2 Fix small typo
Add missing "d" in video.
2020-06-27 10:18:49 -04:00
8ce7425f73 Update link to working dash.js demo (was broken) 2020-06-09 20:13:41 +02:00
1f63806c3f Fixing "missing code in detail" for PlyrEvent type
When using typescript and listening for youtube statechange event, it is missing the code property definition inside the event (even though it is provided in the code).
By making events a map of key-value, we can add easily custom event type for specific event name. Since YouTube "statechange" event differs from the basic PlyrEvent, I added a new Event Type "PlyrStateChangeEvent" having a code property corresponding to a YoutubeState enum defined by the YouTube API documentation.
This pattern follows how addEventListener in the lib.dom.d.ts is defined.
2020-05-05 15:34:41 +02:00
6cb822d56f Merge pull request #1823 from theprojectsomething/features/fullscreen-container
force fullscreen events to trigger on plyr element
2020-05-05 17:07:00 +10:00
9dee5acec6 force fullscreen events to trigger on plyr element (media element in iOS) and not fullscreen container 2020-05-05 16:35:36 +10:00
ef85cb6195 Merge pull request #1822 from zoomerdev/patch-1
Add BitChute to users list
2020-05-05 15:26:29 +10:00
dc8612fcd0 add BitChute to users list 2020-05-05 01:19:42 -04:00
86c3cf648f v3.6.2 2020-05-04 21:38:10 +10:00
37be1336fb v3.6.2 2020-05-04 21:33:16 +10:00
3d7f80737b Package upgrades 2020-05-04 21:32:30 +10:00
58a9321764 Merge pull request #1818 from Bashev/patch-1
Update tooltips.scss
2020-05-04 21:22:21 +10:00
b600f387f0 Merge pull request #1819 from Bashev/patch-2
Update controls.scss
2020-05-04 21:22:05 +10:00
2a97adbec6 Update controls.scss 2020-05-01 18:17:49 +03:00
853753a3da Update tooltips.scss 2020-05-01 18:16:59 +03:00
405bf9ce37 Merge pull request #1815 from taylorchu/progbar
add missing previewThumbnails options
2020-04-29 10:10:39 +10:00
d5ea881729 add missing previewThumbnails options 2020-04-28 17:03:10 -07:00
391b9acd98 Merge pull request #1811 from taylorchu/better-control-2
allow custom control as element
2020-04-29 09:39:50 +10:00
dda2d072aa Merge pull request #1813 from sampotts/master
Merge back
2020-04-29 09:21:32 +10:00
3c1ba2397b allow custom control as element 2020-04-28 15:28:36 -07:00
adb3f35920 v3.6.1 2020-04-28 23:17:54 +10:00
a58b8bf4bb 2 spaces 2020-04-28 22:22:17 +10:00
99a26d65cf Merge pull request #1796 from sampotts/develop
v3.6.0
2020-04-28 22:20:49 +10:00
4915cf0120 Use new feature to set thumbnails on source change in the demo 2020-04-24 01:15:54 +10:00
66c5780616 v3.6.0 2020-04-24 01:05:58 +10:00
e48b1d11ce Housekeeping 2020-04-24 00:47:41 +10:00
ba91f23c50 Fix linting issues 2020-04-24 00:39:26 +10:00
ad4f303aa0 Merge branch 'master' into develop
# Conflicts:
#	readme.md
2020-04-24 00:34:44 +10:00
3b521f73bd Merge branch 'develop' of github.com:sampotts/plyr into develop
# Conflicts:
#	package.json
#	yarn.lock
2020-04-24 00:32:16 +10:00
27126b20bc Merge 2020-04-24 00:31:50 +10:00
68137aa789 Merge pull request #1770 from sampotts/css-variables
Allow customization via CSS Custom Properties
2020-04-24 00:22:39 +10:00
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
a97008aeeb More work on custom properties and documentation 2020-04-24 00:14:50 +10:00
a9b24f5e1f Use custom properties in demo 2020-04-23 22:07:32 +10:00
a9c4e77d1b Migrate custom properties from media to parent 2020-04-23 22:06:36 +10:00
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
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
6aa21c1fae Merge pull request #1789 from likev/patch-1
Update readme.md
2020-04-22 08:31:23 +10:00
8015a961cb Merge pull request #1790 from likev/patch-2
Update readme.md
2020-04-22 08:30:23 +10:00
d2fa69bca6 Update readme.md
fixed code logic error
2020-04-21 23:25:00 +08:00
d63182ecd5 Update readme.md
There is another same Note below
2020-04-21 23:06:06 +08:00
145f2ae24f Poster image fix (fixes #1763) 2020-04-19 20:06:58 +10:00
9c7e429b48 Vimeo ratio fixes 2020-04-19 19:51:06 +10:00
6f1366bd19 Merge pull request #1759 from theprojectsomething/features/fullscreen-container
Features/fullscreen container
2020-04-19 16:51:31 +10:00
502d5977d7 Converted to 2 space indentation 2020-04-11 16:23:14 +10:00
353e19e746 revert dist (brain fart earlier .. deleted instead of reverting) 2020-04-05 12:32:17 +10:00
12ab1ed144 convert fullscreen children listeners logic to ~ES6
remove gulp serve shortcut
2020-04-04 20:00:43 +10:00
11214caf77 revert demo
feature demo is available here: https://codepen.io/theprojectsomething/full/bGdyJmv
2020-04-04 19:57:38 +10:00
c23b4576df remove dist 2020-04-04 14:21:59 +10:00
a55bf00d0f minor demo fix for fallback mode (inline style was overriding plyr stylesheet) 2020-04-04 14:00:03 +10:00
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
44ef0bbc87 include npm run serve shortcut for gulp serve (useful where gulp isn't installed globally) 2020-04-04 13:02:17 +10:00
8f5b59c18c Sentry upgrade for demo 2020-03-30 23:39:01 +11:00
d06881783d Formatting fixes 2020-03-30 17:04:43 +11:00
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
ad63af5096 Added prettier script 2020-03-29 12:13:24 +11:00
09598f07bf Merge branch 'develop' of github.com:sampotts/plyr into develop
# Conflicts:
#	package.json
#	yarn.lock
2020-03-29 12:02:59 +11:00
ef7b30c1b8 Package upgrades 2020-03-29 11:36:51 +11:00
5516db22c3 Gulp file broken down 2020-03-29 11:36:44 +11:00
155add66bd Merge pull request #1686 from lawchihon/master
Added missing full screen options for type definition
2020-03-29 11:22:51 +11:00
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
4935c92b63 Merge pull request #1697 from hug963/fix-vimeo-playback-rate
Fix vimeo playback rate
2020-03-29 11:21:01 +11:00
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
48758bd5f0 Merge pull request #1705 from doublex/master
preview-thumbnails via src:callback()
2020-03-29 11:19:38 +11:00
2f26c80c88 Merge pull request #1739 from ydylla/ignore-internal-play-promises
Ignore internal play promises
2020-03-29 11:18:08 +11:00
be3ffc1f96 Merge pull request #1727 from jnoordsij/fix_shadowroot
Fix shadowroot
2020-03-29 11:17:33 +11:00
3509995756 Build 2020-03-27 23:50:40 +11:00
88dc981f4b Package upgrades 2020-03-27 23:50:36 +11:00
a65acbe43f Merge branch 'master' into develop 2020-03-27 23:37:57 +11:00
6a1d6f13a2 add promise footnote to togglePlay 2020-03-23 22:56:37 +01:00
71928443f3 silence all internal play promises 2020-03-23 22:56:32 +01:00
10f366fe32 update the gitpod setup description to be more precise. 2020-03-16 07:16:22 +00:00
fcd82088a5 Update readme.md 2020-03-14 12:46:33 +11:00
c9b6d9685c Update readme.md 2020-03-14 12:46:05 +11:00
3069d77b74 Merge pull request #1729 from opencollective/opencollective
Add financial contributors for Open Collective
2020-03-14 12:45:17 +11:00
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
99ae4eb3c5 Compare fullscreenElement with shadowroot host if player is in shadow DOM 2020-03-10 09:30:42 +01:00
c7bf0c5c03 Fix prototype used for selector matcher function 2020-03-10 09:19:34 +01:00
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
3c127afeb9 Merge pull request #1706 from sampotts/master
Merge back
2020-02-26 12:33:37 +11:00
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
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
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
6020f95e50 Add missing Typescripts types and options 2020-02-25 11:10:06 +00:00
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
29e62a1e4f Merge pull request #1702 from sampotts/master
Merge back
2020-02-25 09:21:31 +11:00
b12ec094c5 Update contributing.md 2020-02-25 09:20:40 +11:00
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
0cf5d25a7f catch error in setPlaybackRate on Vimeo 2020-02-20 12:57:47 +00:00
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
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
70470ae8d2 Fix issue when controls config is string or element 2020-02-17 18:02:39 +08:00
425b39a762 simplify code contributions by fully automating the dev setup with gitpod 2020-02-17 04:21:35 +00:00
206e3b57d1 v3.5.10 2020-02-14 17:37:32 +00:00
a34bc3ef29 Merge pull request #1692 from sampotts/develop
v3.5.10
2020-02-14 17:33:41 +00:00
6350b7b9e4 v3.5.10
- iOS volume display fix
2020-02-14 17:33:09 +00:00
04d06f2242 v3.5.9 deployed 2020-02-14 17:00:33 +00:00
924049aa14 Merge pull request #1691 from sampotts/develop
v3.5.9
2020-02-14 16:55:56 +00:00
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
7954c92c0b Merge branch 'master' into develop 2020-02-14 16:53:55 +00:00
5afb14283a Fix for regression with volume control width 2020-02-14 16:53:31 +00:00
bfc541b880 Ensure poster image is not downloaded again for HTML5 videos 2020-02-14 16:53:23 +00:00
426280f90c Merge branch 'master' into develop 2020-02-13 15:06:57 +00:00
2e2c5ad72a Styles 2020-02-13 15:06:38 +00:00
ecb091af6b Fix syntax with funding.yml 2020-02-13 12:40:08 +00:00
ecb882b719 Added Open Collective as funding option 2020-02-13 12:39:30 +00:00
cddd9c30db More styles clean up 2020-02-12 14:36:30 +00:00
f6a4625495 Removed redundant keys property 2020-02-12 11:35:47 +00:00
550bd543e3 Added missing full screen options for type definition 2020-02-12 00:06:28 -08:00
a6ff0274a9 v3.5.8 deployed 2020-02-10 18:38:54 +00:00
841746210a Merge pull request #1684 from sampotts/develop
v3.5.8
2020-02-10 18:35:42 +00:00
156abda66a Changelog update 2020-02-10 18:34:26 +00:00
1619510dcf Speed settings logic improvements 2020-02-10 18:34:05 +00:00
ff8dedd4ec Menu border color tweak 2020-02-10 17:53:23 +00:00
637823c8ce Started on v3.5.8 changelog 2020-02-10 11:31:39 +00:00
01219be817 Comment clean up 2020-02-10 11:31:18 +00:00
59e3afba03 Merge branch 'develop' of github.com:sampotts/plyr into develop 2020-02-10 11:28:59 +00:00
db05322ba2 Merge pull request #1670 from ydylla/previewThumbnails-setter
Add previewThumbnails source setter
2020-02-10 11:28:20 +00:00
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
ea350f9d1d Update demo video 2020-02-10 11:25:48 +00:00
eda192639d Demo packages updated 2020-02-10 11:25:39 +00:00
0f16a018ff Set referrerPolicy in the demo 2020-02-10 11:25:25 +00:00
7ca74f48bc Added vimeo options to hide controls and set referrerPolicy 2020-02-10 11:24:38 +00:00
5837c2d5f0 SASS orginasation clean up and flex-direction added 2020-02-10 11:23:57 +00:00
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
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
bb7f7d5e2a 3.5.7 2020-02-09 21:59:40 +00:00
8c44425665 Merge pull request #1679 from sampotts/develop
3.5.7
2020-02-09 21:53:24 +00:00
93e3f8946a Docs updates 2020-02-09 21:52:34 +00:00
95431639a0 Merge branch 'develop' of github.com:sampotts/plyr into develop 2020-02-09 21:42:54 +00:00
3e3186cfeb Update docs for speed options tweaks 2020-02-09 21:42:27 +00:00
2d13ad3d39 Focus trap improvements 2020-02-09 21:42:12 +00:00
d7d0f5fca0 add previewThumbnails to source setter docs 2020-02-09 17:03:31 +01:00
74ba6a96fc Set download attribute for HTML5 only 2020-02-09 10:36:32 +00:00
e1cb2f24f5 Merge pull request #1490 from antonyoneill/develop
Prevent default on settings control click
2020-02-09 10:30:33 +00:00
59e3ef7248 Comments 2020-02-09 10:20:40 +00:00
0b1c480729 Package upgrades 2020-02-08 23:09:50 +00:00
90dc985657 Clean up speed options logic 2020-02-08 23:09:41 +00:00
b5456e1de7 Merge pull request #1671 from ydylla/improve-speed-options
Use the configured speed options
2020-02-08 22:29:49 +00:00
976eebc2a2 Merge pull request #1672 from ydylla/improve-quality-change
Improve/fix quality change state restoring
2020-02-08 22:19:44 +00:00
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
b651d6f027 Merge pull request #1676 from Code1110/develop
Add download attribute to download button
2020-02-08 22:18:14 +00:00
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
0f08c7c13a Ignore quality change if it matches existing 2020-02-08 21:48:51 +00:00
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
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
c33f0995f9 add download attribute to download button 2020-02-07 18:15:56 +01:00
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
299f712cc9 actually use the configured speed options 2020-02-01 16:37:38 +01:00
f755a3c401 preserve playback rate at quality change 2020-02-01 16:35:24 +01:00
472bb479d4 fix regression: not restoring playback state after quality change 2020-02-01 16:35:17 +01:00
61a24eab76 add previewThumbnails source setter #1369 2020-02-01 16:32:19 +01:00
8b9521d5a5 Fix beta deployment 2020-01-30 14:57:14 +00:00
818e1efd43 Deployed 3.5.7-beta.0 2020-01-30 14:56:52 +00:00
58f5380694 Merge pull request #1662 from sampotts/develop
3.5.7
2020-01-30 14:23:40 +00:00
9d51291125 Merge pull request #1663 from sampotts/master
Merge back to beta
2020-01-30 14:23:10 +00:00
fefcca7805 Prepare for 3.5.7 release 2020-01-30 11:34:07 +00:00
5204f33d45 Package upgrades 2020-01-30 11:07:14 +00:00
0041ea3050 Merge branch 'develop' of github.com:sampotts/plyr into develop 2020-01-26 22:05:29 +00:00
f8a28b632c Audio style fix 2020-01-26 22:05:21 +00:00
9c817cff68 Merge pull request #1653 from thatrobotdev/patch-2
Patch 2
2020-01-22 11:32:37 +00:00
521e8abf88 Merge pull request #1654 from thatrobotdev/patch-3
Fix broken link to new plugin
2020-01-22 11:31:02 +00:00
9fe03c7474 Merge pull request #1655 from laukstein/patch-1
Uncaught RangeError: Maximum call stack size exceeded
2020-01-22 11:30:19 +00:00
c3f10e7df3 Uncaught RangeError: Maximum call stack size exceeded
Fix formatTime infinite loop #1621
2020-01-22 08:53:08 +02:00
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
1bb4c207f1 Change vimeo demo video
Change vimeo video to a more general video, fix #1626.
2020-01-21 23:08:55 -07:00
b6da2702a2 Fix reference 2020-01-21 22:30:58 +00:00
fb704b945d Presentation fixes 2020-01-21 22:29:00 +00:00
71d6f59d56 HTML5 poster fixes for multiple downloads 2020-01-21 22:28:48 +00:00
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
b6d94e000f Merge pull request #1651 from shravan2x/develop
Fixed Plyr container not resizing responsively
2020-01-21 22:11:32 +00:00
bfcb7133cb Fixed Plyr container not resizing responsively 2020-01-19 06:05:12 -08:00
7883792ccc Fix issue with browser sync preview 2020-01-14 22:33:01 +00:00
def3668030 Fix issues with fixed ratios and 100% height/width 2020-01-14 22:32:45 +00:00
a77d2d56f6 Fix build 2020-01-14 07:50:48 +00:00
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
53a400027f Remove logic to hide/show volume controls based on audio track 2020-01-14 07:45:24 +00:00
a2498acf7c Manually merge PR #1629 2020-01-14 07:44:59 +00:00
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
6ffaef35cf Manually merged PR #1607 2020-01-14 07:25:41 +00:00
ff105ee203 Fix browser sync vs watch issues 2020-01-14 07:25:04 +00:00
56c0d7bd4d Fix linting issues 2020-01-13 16:38:12 +00:00
d00d31961e Merge pull request #1484 from MaxGiting/patch-1
Improve clarity
2020-01-13 16:32:43 +00:00
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
b2ac730572 Merge pull request #1535 from skerbis/master
adds: REDAXO CMS Plyr AddOn
2020-01-13 16:29:41 +00:00
3424d08d3a Merge pull request #1521 from ondratra/develop
typescript typings
2020-01-13 16:29:15 +00:00
5dd9462bed Merge pull request #1516 from azizhk/toggle_return_promsie
Toggle also returns promise
2020-01-13 16:25:17 +00:00
402eb2b761 Merge pull request #1552 from SoftCreatR/patch-2
Fix ads configuration
2020-01-13 16:24:25 +00:00
63d74eee68 Merge pull request #1560 from 0xflotus/develop
Update readme.md
2020-01-13 16:23:39 +00:00
166a27d094 Comment clean up 2020-01-13 16:22:49 +00:00
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
0b240ae7d1 Fix linting issues 2020-01-13 16:20:02 +00:00
6b0e5cd6f1 Merge branch 'master' into develop 2020-01-13 16:18:38 +00:00
2463434d27 Merge branch 'develop' of github.com:sampotts/plyr into develop 2020-01-13 16:18:24 +00:00
c09b9ac01c Manually port over change from PR #1616 2020-01-13 16:18:05 +00:00
15a1cdde89 Merge pull request #1565 from lmislm/patch-1
Update defaults.js
2020-01-13 16:16:35 +00:00
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
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
8736fa8a52 Manually port over change from PR #1625 2020-01-13 16:04:27 +00:00
cb4dab4250 Merge pull request #1579 from taion/fix-listener-return
fix: Fix handling listener return value
2020-01-13 15:59:36 +00:00
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
93963d3915 Merge pull request #1630 from pjbaert/patch-1
Typo in readme
2020-01-13 15:56:27 +00:00
e7c6f965b4 Merge branch 'master' into develop 2020-01-13 15:49:47 +00:00
4f263ebb1a Added local server, package upgrades 2020-01-13 15:49:29 +00:00
c15bdabf0c Update readme.md 2020-01-12 21:35:40 +00:00
80a077c50a Typo in readme 2019-12-06 16:00:48 +01:00
42d72c5303 fix being unable to unmute autoplayed video on IOS 2019-12-03 10:34:27 -07:00
74e3990604 Merge pull request #1590 from ayunami2000/patch-1
typo
2019-10-23 13:01:56 +11:00
e6e391ad6a typo
change s to z in "Customisable" (now "Customizable")
2019-10-14 15:59:01 -04:00
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
72afffbc8d fix: Fix handling listener return value 2019-09-25 14:21:13 -04:00
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
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
bb43ef15fe Prevents IE11 with resetOnEnd option set to true to play video again 2019-09-16 11:53:07 -03:00
7907652bc9 Update defaults.js
update defaults.i18n
2019-09-06 16:18:40 +08:00
dce665b792 Update readme.md 2019-09-01 18:39:37 +02:00
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
e978bc8690 Fixed ads configuration 2019-08-27 19:28:51 +02:00
91503d3698 Update readme.md 2019-08-15 21:55:22 +02:00
1721f6e9e2 Update readme.md 2019-08-15 21:53:41 +02:00
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
800c8e0a17 typescript typings 2019-08-07 00:08:10 +02:00
d771da9abf Toggle also returns promise 2019-07-30 18:19:10 +05:30
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
aa51719a55 Merge pull request #1497 from ffpetrovic/patch-1
Update index.html
2019-07-12 08:46:42 +10:00
a28685536a Update index.html 2019-07-11 23:18:35 +02:00
400fd77d0a Prevent default on settings icon click 2019-07-04 19:02:22 +01:00
a2bf974058 Improve clarity 2019-07-01 15:09:29 +01:00
7c442c9357 3.5.6 2019-06-21 12:35:47 +10:00
e17e0a81dd Package upgrade 2019-06-21 12:35:22 +10:00
2488299d7b Edge fix 2019-06-21 12:34:49 +10:00
dfc09b8e04 v3.5.5 deployed 2019-06-21 00:24:28 +10:00
6438baaddc Merge branch 'develop' 2019-06-21 00:19:51 +10:00
8fc6c2ba52 File rename and clean up 2019-06-21 00:19:37 +10:00
95092edc93 Merge pull request #1472 from sampotts/develop
v3.5.5
2019-06-21 00:12:10 +10:00
c4b3e0672e Clean up 2019-06-21 00:10:57 +10:00
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
2e40b91ec1 Styling tweaks for demo 2019-06-20 23:50:46 +10:00
97d9228bed Aspect ratio tweaks 2019-06-03 20:13:16 +10:00
0f14865d56 Add duration (commented out) in defaults 2019-06-03 20:12:43 +10:00
c94ab2a39f Repaint clean up 2019-06-03 20:12:21 +10:00
ab89e055de Demo tweaks 2019-06-03 20:11:31 +10:00
ac6e3dba5a Fix for thumbnails in demo for audio 2019-06-03 00:28:09 +10:00
d9d2c4a219 Demo tweaks 2019-06-03 00:26:08 +10:00
1890a9378d Gulp tweaks 2019-06-02 23:16:45 +10:00
ac88e6e190 Demo clean up 2019-06-02 23:16:29 +10:00
15cbae8a19 Removed commented out code for Edge 2019-06-02 22:25:44 +10:00
aaf096d96d Removed raven-js from dependencies 2019-06-02 22:25:17 +10:00
93f5acbffd Fixed cite display 2019-06-02 22:25:00 +10:00
9c717275d2 Packages for demo separated 2019-06-02 22:24:41 +10:00
50a7c2fad6 Build 2019-06-01 20:01:24 +10:00
95be6e721b Fixed merge 2019-06-01 20:01:19 +10:00
dc2e012cc9 Clean up 2019-06-01 20:01:09 +10:00
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
0249772f01 Clean up 2019-06-01 19:50:29 +10:00
5d699d5f47 Merge branch 'develop' of github.com:sampotts/plyr into develop 2019-06-01 19:29:21 +10:00
34d79a5443 Merge pull request #1453 from aFarkas/develop
youtube multiple small issues
2019-06-01 18:46:56 +10:00
1e761e237a Merge pull request #1458 from Jason-Cooke/patch-1
docs: fix typo
2019-06-01 18:46:14 +10:00
36ad132c82 Edge fix 2019-06-01 18:45:12 +10:00
c9055f391b Linting changes 2019-06-01 18:45:07 +10:00
99c95363b4 docs: fix typo 2019-06-01 11:13:44 +12:00
c90526bf07 Create FUNDING.yml 2019-05-27 17:17:05 +10:00
97a6e72e10 fix youtube embed + handle early destroy 2019-05-24 16:55:45 +02:00
f100caba81 fix youtube embed + handle early destroy 2019-05-24 16:53:58 +02:00
80aa6ffe43 Linting changes 2019-04-30 23:44:05 +10:00
0694e58650 v3.5.4 2019-04-25 12:14:48 +10:00
e644eeb5b6 Merge pull request #1423 from sampotts/develop
v3.5.4
2019-04-25 12:11:06 +10:00
5ddd9e02de Fix for the menu in the shadow DOM 2019-04-25 12:09:46 +10:00
a064f0b4fd Merge branch 'develop' of github.com:sampotts/plyr into develop 2019-04-25 12:04:38 +10:00
b9dbcc30fa Eslint config rename 2019-04-25 12:03:42 +10:00
7ab34c7d2e Changelog 2019-04-25 12:03:32 +10:00
17caa3c57b Fix merging class 2019-04-25 12:03:24 +10:00
2e6361898b Package updates 2019-04-25 12:03:13 +10:00
a1b2c0419f Ads improvements for volume and race condition fix 2019-04-25 12:03:04 +10:00
9b81e776fb Styling for control changes 2019-04-25 12:02:48 +10:00
7a4a7dece8 Ratio improvements 2019-04-25 12:02:37 +10:00
f0d3e8c3b9 Fix for className being wiped out 2019-04-25 12:01:52 +10:00
ad68d9484f Clean up and API change 2019-04-25 12:01:30 +10:00
c3fd822857 Notes on autoplay support 2019-04-25 12:01:15 +10:00
e147f3a754 Formatting 2019-04-25 12:01:00 +10:00
b694e7d3ab Use polyfill.io v3 2019-04-22 12:28:26 +10:00
b2fff4c33f Increase speed limits 2019-04-15 22:08:09 +10:00
9c1060d9b0 Merge pull request #1416 from emielbeinema/fix_webcomponents
Make menu work in WebComponent
2019-04-15 21:41:23 +10:00
a91652287b Don't close menu on click in menu in webcomponent 2019-04-15 12:17:10 +02:00
2cf44c236d Use querySelector on container for showMenuPanel 2019-04-15 12:16:48 +02:00
243db9eda3 Fix issue with empty controls and preview thumbs 2019-04-13 12:56:15 +10:00
b675ba1f35 Fix issue with setGutter call 2019-04-13 12:55:48 +10:00
e9367ee85e Fix setting initial speed (fixes #1408) 2019-04-12 20:18:17 +10:00
d9b7928ce6 Merge branch 'master' into develop 2019-04-12 19:00:23 +10:00
cdacae6697 Set download URL via setter 2019-04-12 19:00:17 +10:00
2bd08cdc28 3.5.3 2019-04-12 18:44:05 +10:00
5fefabe3bd Merge pull request #1410 from sampotts/develop
v3.5.3
2019-04-12 18:39:14 +10:00
e281078441 Bump version 2019-04-12 18:38:46 +10:00
9bb75f6f52 Changelog 2019-04-12 18:37:11 +10:00
3b7a24456d Package upgrades 2019-04-12 18:36:55 +10:00
c885d59270 Moved all video styles together 2019-04-12 12:43:45 +10:00
9f30d54963 Merge branch 'master' into develop
# Conflicts:
#	readme.md
2019-04-12 12:40:12 +10:00
b247093495 Aspect ratio improvements (fixes #1042, fixes #1366) 2019-04-12 12:19:48 +10:00
9ca7b861a9 Autoplay tweak for HTML5 2019-04-12 12:14:12 +10:00
2eccf0dd05 Fix YouTube autoplay (fixes #1185) 2019-04-12 12:13:46 +10:00
566b957e65 Merge pull request #1362 from doublex/master
#46 - two patches from 'jamesoflol'
2019-04-11 21:20:23 +10:00
a8456f4ca7 Merge pull request #1404 from taion/clear-timeouts
fix: Properly clear all timeouts on destroy
2019-04-11 21:18:52 +10:00
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
996075decc More work on variable usage 2019-04-11 20:50:20 +10:00
21539be3f2 code cleanup 2019-04-04 09:32:38 +03:00
c22f5c4b39 code cleanup 2019-04-04 08:56:46 +03:00
f4b47a9275 fixed setting youtube host for non-https case 2019-04-04 08:51:20 +03:00
266b70d9d0 fix: Properly clear all timeouts on destroy 2019-04-01 14:42:51 -04:00
6e68ad6d15 Update readme.md 2019-03-26 21:43:59 +11:00
848e798809 Fix merge 2019-03-16 12:15:20 +11:00
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
c202551e6d Merge branch 'develop' of github.com:sampotts/plyr into develop 2019-03-16 11:58:12 +11:00
5b7a025d26 Housekeeping 2019-03-16 11:57:15 +11:00
26bcf83960 Merge pull request #1376 from ar31an/patch-1
Update poster src
2019-03-07 18:59:58 +11:00
e4acff4f8d Update poster src 2019-03-07 12:54:07 +05:00
568ddf2390 Merge pull request #1375 from sampotts/master
Merge back
2019-03-07 18:30:05 +11:00
ce91945544 Preview seek: optional hours and ms in VTT parser 2019-02-27 15:45:24 +01:00
11fed8d1b5 Preview seek: fix: allow absolute thumbnail paths 2019-02-27 15:43:36 +01:00
4c3bf25b8a Fixed issue where the preview thumbnail was present while scrubbing 2019-02-24 12:10:20 +11:00
215fc3677a v3.5.1 2019-02-23 13:14:01 +11:00
4d3b6b882e Merge pull request #1356 from sampotts/develop
v3.5.1
2019-02-23 13:10:20 +11:00
83eda174af v3.5.1 2019-02-23 13:09:26 +11:00
1c29cb890e Merge branch 'master' into develop
# Conflicts:
#	gulpfile.js
2019-02-23 13:08:32 +11:00
438ebe5013 Jsdoc updates 2019-02-23 13:07:35 +11:00
4335a2a0d1 Update .npmignore 2019-02-23 13:07:26 +11:00
825fd292ae Fix build 2019-02-23 13:07:17 +11:00
80990c98c8 Deployed v3.5.0 2019-02-19 01:25:39 +11:00
44d3a17870 Fix links 2019-02-19 01:17:08 +11:00
54110f8358 Update build process 2019-02-19 01:05:59 +11:00
522135adaf Merge branch 'develop' of github.com:sampotts/plyr into develop 2019-02-19 00:19:40 +11:00
153b8dc6bb Added RangeTouch, updated Shr lib in demo 2019-02-19 00:19:25 +11:00
d1bc70ea06 Merge pull request #1327 from robertkraig/develop
Update <progress> bar role=progressbar
2019-02-19 00:11:05 +11:00
df61e5cdd2 Tweak to readme 2019-02-15 00:03:30 +11:00
bf9a557868 Plenty of emojis 2019-02-15 00:02:42 +11:00
537ffce4e0 Docs 2019-02-14 23:44:32 +11:00
b38a481b20 Merge pull request #1343 from electerious/patch-1
Remove NodeList example
2019-02-14 23:29:49 +11:00
7e1d461882 Removed setting up multiple players
This example is already covered in the readme.
2019-02-14 13:29:11 +01:00
c634d3696e Remove NodeList example
I've removed the NodeList example because it's confusing. Only the first element of a NodeList will be used and this is usually not what you want. I've instead replaced it with a `querySelector` and map example. It's related to: https://github.com/sampotts/plyr/issues/801

I know that there was a note, pointing out that the first element will be used, but be honest: Everybody is just scanning the examples for code and doing copy paste :D
2019-02-14 13:16:10 +01:00
0189e90fce Fix deployment 2019-02-12 13:55:45 +11:00
dbd2136bac Fix for cue points missing 2019-02-07 23:45:19 +11:00
eb628c8e4f Ads bug fixes 2019-02-01 00:24:48 +11:00
38cb20706f Merge pull request #1 from robertkraig/defect/update-progress-element-axe-error
Updating accessibility attribute to progressbar
2019-01-30 12:06:37 -08:00
c730866efe Updating accessibility attribute to progressbar
aXe complains this is a non-compatible attribute to be set as `role="presentation"`
2019-01-30 12:05:12 -08:00
d0e3c7c6d0 Merge branch 'develop' into beta 2019-01-29 21:37:06 +11:00
d9daf2c618 Fix gulp 2019-01-29 21:36:47 +11:00
b1da599b5d Merge branch 'develop' into beta 2019-01-29 21:34:40 +11:00
b798368ba6 Update version 2019-01-29 21:33:47 +11:00
fa4868a26d Fix listeners for preview thumbs when changing source 2019-01-29 21:33:16 +11:00
6bf6c3f0f4 Paths 2019-01-27 01:15:54 +11:00
32e8cce527 Fullscreen fix 2019-01-27 01:08:44 +11:00
c125c1a2c0 Added ES builds 2019-01-27 01:08:39 +11:00
d311722cd0 Formatting 2019-01-26 22:47:23 +11:00
1d51b28701 Tweaks 2019-01-26 22:45:47 +11:00
dc54eba8f8 Merge pull request #1319 from fanmio/issues/1316-allow-to-customize-vimeo-url-params
Adds options for vimeo plugin #1316
2019-01-26 17:20:18 +11:00
a84fc396e8 Merge branch 'develop' into issues/1316-allow-to-customize-vimeo-url-params 2019-01-26 17:19:58 +11:00
8b57104f83 Docs for preview thumbs 2019-01-26 17:17:27 +11:00
ff066f0c2a Typo 2019-01-26 16:54:26 +11:00
a64a84f2fe Fix merge 2019-01-26 16:54:04 +11:00
1f0a74f3d5 Changelog typo 2019-01-26 16:52:47 +11:00
44739a17d0 Merge branch 'master' into develop
# Conflicts:
#	changelog.md
#	demo/dist/demo.js.map
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	demo/index.html
#	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
#	package.json
#	readme.md
#	yarn.lock
2019-01-26 16:50:37 +11:00
464acd1a36 Package upgrades 2019-01-26 16:45:30 +11:00
fe536252aa Version bump 2019-01-26 16:42:58 +11:00
52be5ae889 Built files 2019-01-26 16:35:42 +11:00
24319e5c31 Clean up 2019-01-26 16:35:34 +11:00
c44351507f Plugin tweaks for ads and previews 2019-01-26 16:31:47 +11:00
052e426810 Adds options for vimeo plugin #1316
This adds replaces hard coded vimeo options with options that can be passed to the Plyr instance when initializing.
2019-01-24 12:07:01 +01:00
c577eb01ce Style tweaks for preview plugin 2019-01-22 16:24:46 +11:00
263e88f6b3 Comments 2019-01-21 00:39:28 +11:00
4ab8a54a11 Preview design tweaks 2019-01-21 00:32:20 +11:00
ad72ebd4bb Fix analytics 2019-01-17 18:50:26 +11:00
e40c63b9e7 Fix GA 2019-01-17 12:08:25 +11:00
f927d26ce7 v3.4.8
- Calling customized controls function with proper arguments (thanks @a60814billy)
2019-01-17 11:37:19 +11:00
fe24c73c11 Merge branch 'develop' of github.com:sampotts/plyr into develop 2019-01-14 00:34:12 +11:00
2475b2a82b Gulp fixes 2019-01-14 00:34:07 +11:00
4ccc629488 Package upgrades 2019-01-14 00:34:00 +11:00
6782737009 Fullscreen fixes 2019-01-14 00:33:48 +11:00
4d3e188401 Merge branch 'develop' of https://github.com/sampotts/plyr into develop 2019-01-08 23:34:59 +11:00
cd9cbfbd1e Enable thumbs in demo 2019-01-08 23:34:44 +11:00
7dd7c13065 Linting etc 2019-01-08 23:34:28 +11:00
6be125db87 Merge pull request #1292 from omarkhatibco/master
Support Youtube NoCookie mode
2019-01-06 14:10:25 +11:00
1c79ce70c9 Update youtube.js 2019-01-06 14:09:49 +11:00
db2997ef21 Merge branch 'develop' into master 2019-01-06 14:08:46 +11:00
10c88f6e49 Merge pull request #1295 from taion/Math.trunc
fix: Use Math.trunc instead of parseInt
2019-01-06 14:07:07 +11:00
6d9d315ca7 fix: Use Math.trunc instead of parseInt 2018-12-20 13:06:29 -05:00
973527cb84 Merge pull request #1294 from smnbbrv/patch-1
Add Angular plugin reference
2018-12-19 08:00:49 +07:00
0ff47fe3ea Add Angular plugin reference 2018-12-18 17:27:07 +01:00
5fdc0aae66 Merge pull request #1253 from jamesoflol/preview-thumbs
Preview seek/scrubbing thumbnails
2018-12-15 10:43:36 +07:00
d97257a5a9 Preview seek: Edge+IE11 fixes
- Fixed bug: Edge seek errors: Replaced array spread with Array.from()
- Fixed IE11 bug: seek time was offset to the left. Required an extra container div to facilitate this
2018-12-15 11:32:50 +11:00
279f051905 Preview seek: image preloading + tweaks/fixes
- Preloads neighbouring images after showing current image
- Re-fixed bug: if you mousedown but don't move mouse, it shows a stale image in the scrubbing container
- Fixed bug: mobile device correctly detect touch
2018-12-14 12:50:29 +11:00
88ffd0f138 remove comment 2018-12-13 17:31:15 +01:00
11618353ea support Youtube noCookie Mode 2018-12-13 17:30:10 +01:00
e948bfd585 Preview seek: jpeg sprites + much more
- Allow jpeg sprites - much snappier and more accurate
- Fixed bug: right clicking the seek bar sticks on mousedown
- Fixed bug: moving the mouse really quickly results in not updating the thumb
- Fixed bug: if you mousedown but don't move mouse, it shows a stale image in the scrubbing container
- Fixed bug: very first image shows as 0px
- Fixed bug: stretches images when video isn't same aspect as player
2018-12-13 20:39:39 +11:00
bdd513635f Work on outline/focus styles 2018-12-08 17:06:20 +11:00
2bbebd811b Merge pull request #1267 from gurupras/issue-858
Ensure custom handlers are called on container clicks that trigger togglePlay or restart
2018-12-08 16:58:51 +11:00
3fb85664d2 Updated restart logic to call play instead of togglePlay 2018-12-08 00:57:47 -05:00
b9ea9fba9a Merge branch 'develop' of https://github.com/sampotts/plyr into develop 2018-12-08 16:50:48 +11:00
a0303969c2 Fix for error when mime type not specified (fixes #1274) 2018-12-08 16:50:44 +11:00
fa78df3749 Merge pull request #1273 from samuelgozi/patch-1
Fix buffer progress bar animation on WebKit browsers
2018-12-08 16:27:39 +11:00
df5b7a008d Fix: buffer progress bar transition on webkit
The transition was set on the wrong pseudo element for WebKit browsers.
2018-11-22 19:35:01 +02:00
80813b0406 Replaced calls to player.restart() and player.togglePlay() with proxy(...) to ensure that custom handlers are called 2018-11-19 23:06:23 -05:00
e8d2f23b81 Merge pull request #1254 from a60814billy/fix/custom-controls-calling-argument
Calling customized controls function with proper arguments
2018-11-12 21:32:31 +11:00
0e181133c1 Calling customized controls function with proper arguments 2018-11-12 15:37:46 +08:00
8f27611911 Preview seek/scrubbing thumbnails 2018-11-12 15:55:26 +11:00
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
2c8a337f26 v3.4.7
-   Fix for Vimeo fullscreen with non native aspect ratios (fixes #854)
2018-11-08 23:34:10 +11:00
c24e52d97d Package updates 2018-11-08 23:29:54 +11:00
574f40949c Merge branch 'master' into develop 2018-11-08 23:19:57 +11:00
cf3848fbd5 Merge branch 'develop' of github.com:sampotts/plyr into develop 2018-11-08 23:19:26 +11:00
a19ad69038 Fix for Vimeo fullscreen with non 16:9 aspect ratios 2018-11-08 23:19:11 +11:00
d6f20e2756 Package upgrades 2018-11-08 23:18:23 +11:00
e2fc20ca76 Styling tweaks 2018-11-08 23:18:04 +11:00
37c3f7109d Additional listener for checking for audio tracks 2018-11-08 23:17:44 +11:00
99d5211a33 Merge pull request #1247 from danielcgold/patch-1
[Edit README] Halfhalftravel uses Plyr!
2018-11-06 10:17:55 +11:00
b97f143195 Halfhalftravel uses Plyr!
https://www.halfhalftravel.com/events/medellin-photo-walk.html
https://www.halfhalftravel.com/travel-guides/how-to-travel-to-santa-fe-de-antioquia-colombia.html
2018-11-05 10:57:23 -05:00
e8da4326b6 Prevent scroll on focus 2018-11-03 21:17:46 +11:00
67f908aa8d Load media after UI is built 2018-11-03 21:17:32 +11:00
65eb5c1b8b Fix support check 2018-11-03 21:16:40 +11:00
7d484c6e09 Merge pull request #1232 from tocsinde/patch-1
Readme: Add missing annotation to PIP support
2018-10-26 08:37:33 +11:00
8252e13eb9 Readme: Add missing annotation to PIP support
PIP only works on HTML videos, so I added the number of the (already existing) annotation.
2018-10-25 23:35:20 +02:00
1a9b860e68 v3.4.6
-   Added picture-in-picture support for Chrome 70+
-   Fixed issue with versioning the SVG sprite in the gulp build script
2018-10-25 09:44:40 +11:00
cede7d0f35 Fix gulp build 2018-10-25 09:21:37 +11:00
fe26d383f1 Added support for picture-in-picture in Chrome 2018-10-25 09:17:15 +11:00
df4bc268dc Merge branch 'master' into develop 2018-10-25 00:14:14 +11:00
67b7262764 Revert PR #1211 2018-10-24 23:00:54 +11:00
88528ef979 Merge pull request #1197 from TechGuard/fix-html5-quality-settings
Fix html5 quality settings
2018-10-24 22:39:34 +11:00
b6175b1ca9 Merge branch 'develop' into fix-html5-quality-settings 2018-10-24 22:39:10 +11:00
aa20ebaa9c Merge pull request #1211 from melbahja/develop
duration after changing video quality
2018-10-24 22:37:49 +11:00
06db3f702d Update plyr.js 2018-10-13 13:23:42 +01:00
a2a82a96a6 fix: continue with the current duration after changing video quality 2018-10-13 12:59:59 +01:00
a86bbae851 Only save quality setting when it's updated by the user. Fixes bug in html5 player where it would override the settings if the current video does not support the given quality. 2018-09-29 21:23:10 +02:00
3e0a911418 WIP 2018-05-26 13:37:10 +10:00
afc969bac3 Merge branch 'beta' of github.com:Selz/plyr into beta 2018-05-08 22:22:16 +10:00
e1ff516219 Merge branch 'master' into beta 2018-05-08 22:22:09 +10:00
8efa46aeab Merge branch 'master' into beta 2018-04-27 21:42:13 +10:00
160 changed files with 21349 additions and 36031 deletions

View File

@ -4,7 +4,7 @@ root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

18
.eslintrc Normal file
View File

@ -0,0 +1,18 @@
{
"parser": "babel-eslint",
"extends": ["@sampotts/eslint-config/es6"],
"env": {
"browser": true,
"es6": true
},
"globals": {
"Plyr": false,
"jQuery": false
},
"rules": {
"import/no-cycle": "warn"
},
"parserOptions": {
"sourceType": "module"
}
}

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
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,5 @@
# These are supported funding model platforms
github: sampotts
patreon: plyr
open_collective: plyr

View File

@ -1,3 +1,3 @@
PLEASE USE OUR SPECIFIC ISSUE TEMPLATES for bug reports, features and improvement suggestions.
Our issue tracker is not for support questions. If you need help, follow our support instructions: https://github.com/sampotts/plyr/blob/master/contributing.md#support
Our issue tracker is not for support questions. If you need help, follow our support instructions: https://github.com/sampotts/plyr/blob/master/CONTRIBUTING.md#support

View File

@ -1,8 +1,3 @@
### Link to related issue (if applicable)
### Summary of proposed changes
### Checklist
- [ ] Use `develop` as the base branch
- [ ] Exclude the gulp build (`/dist` changes) from the PR
- [ ] Test on [supported browsers](https://github.com/sampotts/plyr#browser-support)

4
.gitignore vendored
View File

@ -2,10 +2,8 @@ node_modules
.DS_Store
credentials.json
*.mp4
!dist/blank.mp4
index-*.html
npm-debug.log
yarn-error.log
package-lock.json
*.webm
.idea/
dist/

6
.gitpod.yml Normal file
View File

@ -0,0 +1,6 @@
tasks:
- before: npm install && npm i gulp -g
command: gulp
ports:
- port: 3000
onOpen: open-preview

View File

@ -2,10 +2,11 @@ demo
.github
.vscode
*.code-workspace
build.json
credentials.json
bundles.json
deploy.json
yarn.lock
package-lock.json
*.mp4
*.webm
!dist/blank.mp4
*.mp4
!dist/blank.mp4

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v12.8

View File

@ -1,7 +1,7 @@
{
"useTabs": false,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120
"useTabs": false,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120
}

View File

@ -1,25 +1,25 @@
{
"plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"],
"extends": ["stylelint-config-recommended", "stylelint-config-sass-guidelines", "stylelint-config-prettier"],
"rules": {
"selector-class-pattern": null,
"selector-no-qualifying-type": [
true,
{
"ignore": ["attribute", "class"]
}
],
"string-no-newline": null,
"indentation": 4,
"string-quotes": "single",
"max-nesting-depth": 2,
"plugin/selector-bem-pattern": {
"preset": "bem",
"componentName": "(([a-z0-9]+(?!-$)-?)+)",
"componentSelectors": {
"initial": "\\.{componentName}(((__|--)(([a-z0-9\\[\\]'=]+(?!-$)-?)+))+)?$"
},
"ignoreSelectors": [".*\\.has-.*", ".*\\.is-.*"]
}
"plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"],
"extends": ["stylelint-config-recommended", "stylelint-config-sass-guidelines", "stylelint-config-prettier"],
"rules": {
"selector-class-pattern": null,
"selector-no-qualifying-type": [
true,
{
"ignore": ["attribute", "class"]
}
],
"string-no-newline": null,
"indentation": 2,
"string-quotes": "single",
"max-nesting-depth": 2,
"plugin/selector-bem-pattern": {
"preset": "bem",
"componentName": "(([a-z0-9]+(?!-$)-?)+)",
"componentSelectors": {
"initial": "\\.{componentName}(((__|--)(([a-z0-9\\[\\]'=]+(?!-$)-?)+))+)?$"
},
"ignoreSelectors": [".*\\.has-.*", ".*\\.is-.*"]
}
}
}

View File

@ -2,7 +2,5 @@ language: node_js
node_js: lts/*
script:
- bash .travis/prevent-base-master.sh
- bash .travis/omit-dist.sh
- npm run lint
- npm run build

View File

@ -1,5 +0,0 @@
#!/bin/bash
if [ $TRAVIS_BRANCH == "develop" ] && $(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qE "^(demo/)?dist/"); then
echo 'Build output ("dist" and "demo/dist") not permitted in develop' >&2
exit 1
fi

View File

@ -1,5 +0,0 @@
#!/bin/bash
if [ "$TRAVIS_PULL_REQUEST" != "false" ] && [ $TRAVIS_BRANCH == "master" ] && $(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -q "^src/"); then
echo 'The base branch for pull requests must be "develop"' >&2
exit 1
fi

View File

@ -2,11 +2,11 @@
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
"dbaeumer.vscode-eslint",
"wix.vscode-import-cost",
"esbenp.prettier-vscode",
"shinnn.stylelint",
"wayou.vscode-todo-highlight"
"wayou.vscode-todo-highlight",
"wix.vscode-import-cost",
"stylelint.vscode-stylelint",
"pflannery.vscode-versionlens"
]
}

1228
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

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).
If these doesn't answer your question
* Use [Stack Overflow](https://stackoverflow.com/) for questions that doesn't directly involve Plyr. This includes for example how to use Javascript, CSS or HTML5 media in general, and how to use other frameworks, libraries and technology.
* Use [our Slack](https://bit.ly/plyr-chat) if you need help using Plyr or have questions about Plyr.
- Use [Stack Overflow](https://stackoverflow.com/) for questions that doesn't directly involve Plyr. This includes for example how to use Javascript, CSS or HTML5 media in general, and how to use other frameworks, libraries and technology.
- Use [our Slack](https://bit.ly/plyr-chat) if you need help using Plyr or have questions about Plyr.
## Commenting
When commenting, keep a civil tone and stay on topic. Don't ask for [support](#support), or post "+1" or "I agree" type of comments. Use the emojis instead.
Asking for the status on issues is discouraged. Unless someone has explicitly said in an issue that it's work in progress, most likely that means no one is working on it. We have a lot to do, and it may not be a top priority for us.
We *may* moderate discussions. We do this to avoid threads being "hijacked", to avoid confusion in case the content is misleading or outdated, and to avoid bothering people with github notifications.
We _may_ moderate discussions. We do this to avoid threads being "hijacked", to avoid confusion in case the content is misleading or outdated, and to avoid bothering people with github notifications.
## Creating issues
@ -23,18 +25,30 @@ Please follow the instructions in our issue templates. Don't use github issues t
## Contributing features and documentation
* If you want to add a feature or make critical changes, you may want to ensure that this is something we also want (so you don't waste your time). Ask us about this in the corresponding issue if there is one, or on [our Slack](https://bit.ly/plyr-chat) otherwise.
- If you want to add a feature or make critical changes, you may want to ensure that this is something we also want (so you don't waste your time). Ask us about this in the corresponding issue if there is one, or on [our Slack](https://bit.ly/plyr-chat) otherwise.
* Fork Plyr, and create a new branch in your fork, based on the **develop** branch
- Fork Plyr, and create a new branch in your fork, based on the **develop** branch
* To test locally, you can use the demo. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build while you are working, and run a local server from the repository root directory. If you have Python installed, this command should work: `python -m SimpleHTTPServer 8080`. Then go to `http://localhost:8080/demo/`
- To test locally, you can use the demo site. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build and it will run a local web server for development and watch for any changes.
* Develop and test your modifications.
### Online one-click setup for contributing
* Preferably commit your changes as independent logical chunks, with meaningful messages. Make sure you do not commit unnecessary files or changes, such as the build output, or logging and breakpoints you added for testing.
You can use Gitpod (a free online VS Code-like IDE) for contributing. With a single click it will launch a workspace and automatically:
* If your modifications changes the documented behavior or add new features, document these changes in readme.md.
- clone the plyr repo.
- install the dependencies with `yarn install` in root directory and "demo" directory.
- run `gulp` in root directory to start the dev server.
* When finished, push the changes to your GitHub repository and send a pull request to **develop**. Describe what your PR does.
So that you can start straight away.
* If the Travis build fails, or if you get a code review with change requests, you can fix these by pushing new or rebased commits to the branch.
[![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. 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.

File diff suppressed because it is too large Load Diff

48
build.json Normal file
View File

@ -0,0 +1,48 @@
{
"js": {
"plyr.js": {
"src": "./src/js/plyr.js",
"dist": "./dist/",
"formats": ["es", "umd"],
"namespace": "Plyr"
},
"plyr.polyfilled.js": {
"src": "./src/js/plyr.polyfilled.js",
"dist": "./dist/",
"formats": ["es", "umd"],
"namespace": "Plyr",
"polyfill": true
},
"demo.js": {
"src": "./demo/src/js/demo.js",
"dist": "./demo/dist/",
"formats": ["iife"],
"namespace": "Demo",
"polyfill": true
}
},
"css": {
"plyr.css": {
"src": "./src/sass/plyr.scss",
"dist": "./dist/"
},
"demo.css": {
"src": "./demo/src/sass/bundles/demo.scss",
"dist": "./demo/dist/"
},
"error.css": {
"src": "./demo/src/sass/bundles/error.scss",
"dist": "./demo/dist/"
}
},
"sprite": {
"plyr.svg": {
"src": "./src/sprite/*.svg",
"dist": "./dist"
},
"demo.svg": {
"src": "./src/sprite/*.svg",
"dist": "./demo/dist"
}
}
}

View File

@ -1,20 +0,0 @@
{
"plyr": {
"sass": {
"plyr.css": "src/sass/plyr.scss"
},
"js": {
"plyr.js": "src/js/plyr.js",
"plyr.polyfilled.js": "src/js/plyr.polyfilled.js"
}
},
"demo": {
"sass": {
"demo.css": "demo/src/sass/bundles/demo.scss",
"error.css": "demo/src/sass/bundles/error.scss"
},
"js": {
"demo.js": "demo/src/js/demo.js"
}
}
}

File diff suppressed because it is too large Load Diff

1
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

4356
demo/dist/demo.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
demo/dist/error.css vendored

File diff suppressed because one or more lines are too long

View File

@ -1,191 +1,268 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<head>
<meta charset="utf-8" />
<title>Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player</title>
<meta name="description" property="og:description" content="A simple HTML5 media player with custom controls and WebVTT captions.">
<meta name="author" content="Sam Potts">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta
name="description"
property="og:description"
content="A simple HTML5 media player with custom controls and WebVTT captions."
/>
<meta name="author" content="Sam Potts" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Icons -->
<link rel="icon" href="https://cdn.plyr.io/static/icons/favicon.ico">
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16">
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png">
<link rel="icon" href="https://cdn.plyr.io/static/icons/favicon.ico" />
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16" />
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png" />
<!-- Opengraph -->
<meta property="og:title" content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player">
<meta property="og:site_name" content="Plyr">
<meta property="og:url" content="https://plyr.io">
<meta property="og:image" content="https://cdn.plyr.io/static/icons/1200x630.png">
<!-- Open Graph -->
<meta property="og:title" content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player" />
<meta property="og:site_name" content="Plyr" />
<meta property="og:url" content="https://plyr.io" />
<meta property="og:image" content="https://cdn.plyr.io/static/icons/1200x630.png" />
<!-- Twitter -->
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@sam_potts">
<meta name="twitter:creator" content="@sam_potts">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@sam_potts" />
<meta name="twitter:creator" content="@sam_potts" />
<meta name="twitter:card" content="summary_large_image" />
<!-- Docs styles -->
<link rel="stylesheet" href="dist/demo.css?v=2">
<link rel="stylesheet" href="dist/demo.css" />
<!-- Preload -->
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2">
</head>
<link
rel="preload"
as="font"
crossorigin
type="font/woff2"
href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2"
/>
<link
rel="preload"
as="font"
crossorigin
type="font/woff2"
href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2"
/>
<body>
<!-- Google Analytics-->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-132699580-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-132699580-1');
</script>
</head>
<body>
<div class="grid">
<header>
<h1>Plyr</h1>
<p>A simple, accessible and customisable media player for
<button type="button" class="faux-link" data-source="video">
<svg class="icon">
<title>HTML5</title>
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
</svg>Video</button>,
<button type="button" class="faux-link" data-source="audio">
<svg class="icon">
<title>HTML5</title>
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
</svg>Audio</button>,
<button type="button" class="faux-link" data-source="youtube">
<svg class="icon" role="presentation">
<title>YouTube</title>
<path d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
<header>
<h1>Pl<span>a</span>y<span>e</span>r</h1>
<p>
A simple, accessible and customisable media player for
<button type="button" class="faux-link" data-source="video">
<svg class="icon">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path></svg
>Video</button
>,
<button type="button" class="faux-link" data-source="audio">
<svg class="icon">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path></svg
>Audio</button
>,
<button type="button" class="faux-link" data-source="youtube">
<svg class="icon" role="presentation">
<title>YouTube</title>
<path
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
M6,11V5l5,3L6,11z"></path>
</svg>YouTube</button> and
<button type="button" class="faux-link" data-source="vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
<path d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
M6,11V5l5,3L6,11z"
></path></svg
>YouTube
</button>
and
<button type="button" class="faux-link" data-source="vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
<path
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"></path>
</svg>Vimeo</button>
</p>
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"
></path></svg
>Vimeo
</button>
</p>
<p>Premium video monitization from
<a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border">
<img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi">
<span class="sr-only">ai.vi</span>
</a>
</p>
<p>
Premium video monetization from
<a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border">
<img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi" />
<span class="sr-only">ai.vi</span>
</a>
</p>
<div class="call-to-action">
<span class="button--with-count">
<a href="https://github.com/sampotts/plyr" target="_blank" class="button" data-shr-network="github">
<svg class="icon" role="presentation">
<title>GitHub</title>
<path d="M8,0.2c-4.4,0-8,3.6-8,8c0,3.5,2.3,6.5,5.5,7.6
<div class="call-to-action">
<a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr">
<svg class="icon" role="presentation">
<title>GitHub</title>
<path
d="M8,0.2c-4.4,0-8,3.6-8,8c0,3.5,2.3,6.5,5.5,7.6
C5.9,15.9,6,15.6,6,15.4c0-0.2,0-0.7,0-1.4C3.8,14.5,3.3,13,3.3,13c-0.4-0.9-0.9-1.2-0.9-1.2c-0.7-0.5,0.1-0.5,0.1-0.5
c0.8,0.1,1.2,0.8,1.2,0.8C4.4,13.4,5.6,13,6,12.8c0.1-0.5,0.3-0.9,0.5-1.1c-1.8-0.2-3.6-0.9-3.6-4c0-0.9,0.3-1.6,0.8-2.1
c-0.1-0.2-0.4-1,0.1-2.1c0,0,0.7-0.2,2.2,0.8c0.6-0.2,1.3-0.3,2-0.3c0.7,0,1.4,0.1,2,0.3c1.5-1,2.2-0.8,2.2-0.8
c0.4,1.1,0.2,1.9,0.1,2.1c0.5,0.6,0.8,1.3,0.8,2.1c0,3.1-1.9,3.7-3.7,3.9C9.7,12,10,12.5,10,13.2c0,1.1,0,1.9,0,2.2
c0,0.2,0.1,0.5,0.6,0.4c3.2-1.1,5.5-4.1,5.5-7.6C16,3.8,12.4,0.2,8,0.2z"></path>
</svg>
Download on GitHub
</a>
</span>
</div>
</header>
c0,0.2,0.1,0.5,0.6,0.4c3.2-1.1,5.5-4.1,5.5-7.6C16,3.8,12.4,0.2,8,0.2z"
></path>
</svg>
Download on GitHub
</a>
</div>
</header>
<main>
<div id="container">
<video
controls
crossorigin
playsinline
data-poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg"
id="player"
>
<!-- Video files -->
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4"
type="video/mp4"
size="576"
/>
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4"
type="video/mp4"
size="720"
/>
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4"
type="video/mp4"
size="1080"
/>
<main>
<div id="container">
<video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
<!-- Video files -->
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
<!-- Caption files -->
<track
kind="captions"
label="English"
srclang="en"
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
default
/>
<track
kind="captions"
label="Français"
srclang="fr"
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"
/>
<!-- Caption files -->
<track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
default>
<track kind="captions" label="Français" srclang="fr" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
<!-- Fallback for browsers that don't support the <video> element -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
</video>
</div>
<!-- Fallback for browsers that don't support the <video> element -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
</video>
</div>
<ul>
<li class="plyr__cite plyr__cite--video" hidden>
<small>
<svg class="icon">
<title>HTML5</title>
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
</svg>
<a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank">View From A Blue Moon</a> &copy; Brainfarm
</small>
</li>
<li class="plyr__cite plyr__cite--audio" hidden>
<small>
<svg class="icon" title="HTML5">
<title>HTML5</title>
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
</svg>
<a href="http://www.kishibashi.com/" target="_blank">Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;</a> &copy; Kishi Bashi
</small>
</li>
<li class="plyr__cite plyr__cite--youtube" hidden>
<small>
<a href="https://www.youtube.com/watch?v=bTqVqk7FSmY" target="_blank">View From A Blue Moon</a> on&nbsp;
<span class="color--youtube">
<svg class="icon" role="presentation">
<title>YouTube</title>
<path d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
<ul>
<li class="plyr__cite plyr__cite--video" hidden>
<small>
<svg class="icon">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path>
</svg>
<a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank"
>View From A Blue Moon</a
>
&copy; Brainfarm
</small>
</li>
<li class="plyr__cite plyr__cite--audio" hidden>
<small>
<svg class="icon" title="HTML5">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path>
</svg>
<a href="http://www.kishibashi.com/" target="_blank"
>Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;</a
>
&copy; Kishi Bashi
</small>
</li>
<li class="plyr__cite plyr__cite--youtube" hidden>
<small>
<a href="https://www.youtube.com/watch?v=bTqVqk7FSmY" target="_blank">View From A Blue Moon</a>
on&nbsp;
<span class="color--youtube">
<svg class="icon" role="presentation">
<title>YouTube</title>
<path
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
M6,11V5l5,3L6,11z"></path>
</svg>YouTube
</span>
</small>
</li>
<li class="plyr__cite plyr__cite--vimeo" hidden>
<small>
<a href="https://vimeo.com/76979871" target="_blank">The New Vimeo Player</a> on&nbsp;
<span class="color--vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
<path d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
M6,11V5l5,3L6,11z"
></path></svg
>YouTube
</span>
</small>
</li>
<li class="plyr__cite plyr__cite--vimeo" hidden>
<small>
<a href="https://vimeo.com/40648169" target="_blank">Toob “Wavaphon” Music Video</a>
on&nbsp;
<span class="color--vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
<path
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"></path>
</svg>Vimeo
</span>
</small>
</li>
</ul>
</main>
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"
></path></svg
>Vimeo
</span>
</small>
</li>
</ul>
</main>
</div>
<aside>
<svg class="icon">
<title>Twitter</title>
<path d="M16,3c-0.6,0.3-1.2,0.4-1.9,0.5c0.7-0.4,1.2-1,1.4-1.8c-0.6,0.4-1.3,0.6-2.1,0.8c-0.6-0.6-1.5-1-2.4-1
<svg class="icon">
<title>Twitter</title>
<path
d="M16,3c-0.6,0.3-1.2,0.4-1.9,0.5c0.7-0.4,1.2-1,1.4-1.8c-0.6,0.4-1.3,0.6-2.1,0.8c-0.6-0.6-1.5-1-2.4-1
C9.3,1.5,7.8,3,7.8,4.8c0,0.3,0,0.5,0.1,0.7C5.2,5.4,2.7,4.1,1.1,2.1c-0.3,0.5-0.4,1-0.4,1.7c0,1.1,0.6,2.1,1.5,2.7
c-0.5,0-1-0.2-1.5-0.4c0,0,0,0,0,0c0,1.6,1.1,2.9,2.6,3.2C3,9.4,2.7,9.4,2.4,9.4c-0.2,0-0.4,0-0.6-0.1c0.4,1.3,1.6,2.3,3.1,2.3
c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"></path>
</svg>
<p>If you think Plyr's good,
<a href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts"
target="_blank" data-shr-network="twitter">tweet it</a> 👍
</p>
c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"
></path>
</svg>
<p>
If you think Plyr's good,
<a
href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts"
target="_blank"
class="js-shr"
>tweet it</a
>
👍
</p>
</aside>
<!-- Polyfills -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL"
crossorigin="anonymous"></script>
<!-- Plyr core script -->
<script src="../dist/plyr.js" crossorigin="anonymous"></script>
<!-- Sharing libary (https://shr.one) -->
<script src="https://cdn.shr.one/1.0.1/shr.js" crossorigin="anonymous"></script>
<!-- Rangetouch to fix <input type="range"> on touch devices (see https://rangetouch.com) -->
<script src="https://cdn.rangetouch.com/1.0.1/rangetouch.js" async crossorigin="anonymous"></script>
<!-- Docs script -->
<script src="dist/demo.js" crossorigin="anonymous"></script>
</body>
</html>
</body>
</html>

14
demo/package.json Normal file
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.22.3",
"core-js": "^3.6.5",
"custom-event-polyfill": "^1.0.7",
"shr-buttons": "2.0.3",
"url-polyfill": "^1.1.10"
}
}

View File

@ -1,302 +1,149 @@
// ==========================================================================
// Plyr.io demo
// This code is purely for the https://plyr.io website
// Please see readme.md in the root or github.com/sampotts/plyr
// Please see README.md in the root or github.com/sampotts/plyr
// ==========================================================================
import Raven from 'raven-js';
import './tab-focus';
import 'custom-event-polyfill';
import 'url-polyfill';
import * as Sentry from '@sentry/browser';
import Shr from 'shr-buttons';
import Plyr from '../../../src/js/plyr';
import sources from './sources';
import toggleClass from './toggle-class';
(() => {
const { host } = window.location;
const env = {
prod: host === 'plyr.io',
dev: host === 'dev.plyr.io',
};
const production = 'plyr.io';
document.addEventListener('DOMContentLoaded', () => {
Raven.context(() => {
const selector = '#player';
const container = document.getElementById('container');
// Sentry for demo site (https://plyr.io) only
if (window.location.host === production) {
Sentry.init({
dsn: 'https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555',
whitelistUrls: [production].map((d) => new RegExp(`https://(([a-z0-9])+(.))*${d}`)),
});
}
if (window.shr) {
window.shr.setup({
count: {
classname: 'button__count',
},
});
}
document.addEventListener('DOMContentLoaded', () => {
const selector = '#player';
// Setup tab focus
const tabClassName = 'tab-focus';
// Remove class on blur
document.addEventListener('focusout', event => {
if (!event.target.classList || container.contains(event.target)) {
return;
}
event.target.classList.remove(tabClassName);
});
// Add classname to tabbed elements
document.addEventListener('keydown', event => {
if (event.keyCode !== 9) {
return;
}
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
const focused = document.activeElement;
if (!focused || !focused.classList || container.contains(focused)) {
return;
}
focused.classList.add(tabClassName);
}, 10);
});
// Setup the player
const player = new Plyr(selector, {
debug: true,
title: 'View From A Blue Moon',
iconUrl: '../dist/plyr.svg',
keyboard: {
global: true,
},
tooltips: {
controls: true,
},
captions: {
active: true,
},
keys: {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: {
enabled: env.prod || env.dev,
publisherId: '918848828995742',
},
});
// Expose for tinkering in the console
window.player = player;
// Setup type toggle
const buttons = document.querySelectorAll('[data-source]');
const types = {
video: 'video',
audio: 'audio',
youtube: 'youtube',
vimeo: 'vimeo',
};
let currentType = window.location.hash.replace('#', '');
const historySupport = window.history && window.history.pushState;
// Toggle class on an element
function toggleClass(element, className, state) {
if (element) {
element.classList[state ? 'add' : 'remove'](className);
}
}
// Set a new source
function newSource(type, init) {
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
if (
!(type in types) ||
(!init && type === currentType) ||
(!currentType.length && type === types.video)
) {
return;
}
switch (type) {
case types.video:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
type: 'video/mp4',
size: 576,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
type: 'video/mp4',
size: 720,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
type: 'video/mp4',
size: 1080,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
type: 'video/mp4',
size: 1440,
},
],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
};
break;
case types.audio:
player.source = {
type: 'audio',
title: 'Kishi Bashi &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;
// Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
// Set active on parent
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
cite.setAttribute('hidden', '');
});
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
}
// Bind to each button
Array.from(buttons).forEach(button => {
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
newSource(type);
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
}
});
});
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && 'type' in event.state) {
newSource(event.state.type);
}
});
// On load
if (historySupport) {
const video = !currentType.length;
// If there's no current type set, assume video
if (video) {
currentType = types.video;
}
// Replace current history state
if (currentType in types) {
window.history.replaceState(
{
type: currentType,
},
'',
video ? '' : `#${currentType}`,
);
}
// If it's not video, load the source
if (currentType !== types.video) {
newSource(currentType, true);
}
}
});
// Setup share buttons
Shr.setup('.js-shr', {
count: {
className: 'button__count',
},
wrapper: {
className: 'button--with-count',
},
});
// Raven / Sentry
// For demo site (https://plyr.io) only
if (env.prod) {
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
// Setup the player
const player = new Plyr(selector, {
debug: true,
title: 'View From A Blue Moon',
iconUrl: 'dist/demo.svg',
keyboard: {
global: true,
},
tooltips: {
controls: true,
},
captions: {
active: true,
},
ads: {
enabled: window.location.host.includes(production),
publisherId: '918848828995742',
},
previewThumbnails: {
enabled: true,
src: ['https://cdn.plyr.io/static/demo/thumbs/100p.vtt', 'https://cdn.plyr.io/static/demo/thumbs/240p.vtt'],
},
vimeo: {
// Prevent Vimeo blocking plyr.io demo site
referrerPolicy: 'no-referrer',
},
});
// Expose for tinkering in the console
window.player = player;
// Setup type toggle
const buttons = document.querySelectorAll('[data-source]');
const types = Object.keys(sources);
const historySupport = Boolean(window.history && window.history.pushState);
let currentType = window.location.hash.substring(1);
const hasInitialType = currentType.length;
function render(type) {
// Remove active classes
Array.from(buttons).forEach((button) => toggleClass(button.parentElement, 'active', false));
// Set active on parent
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach((cite) => {
// eslint-disable-next-line no-param-reassign
cite.hidden = true;
});
document.querySelector(`.plyr__cite--${type}`).hidden = false;
}
// Google analytics
// For demo site (https://plyr.io) only
/* eslint-disable */
if (env.prod) {
((i, s, o, g, r, a, m) => {
i.GoogleAnalyticsObject = r;
i[r] =
i[r] ||
function() {
(i[r].q = i[r].q || []).push(arguments);
};
i[r].l = 1 * new Date();
a = s.createElement(o);
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m);
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
window.ga('create', 'UA-40881672-11', 'auto');
window.ga('send', 'pageview');
// 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);
}
/* eslint-enable */
// Bind to each button
Array.from(buttons).forEach((button) => {
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
setSource(type);
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
}
});
});
// List for backwards/forwards
window.addEventListener('popstate', (event) => {
if (event.state && Object.keys(event.state).includes('type')) {
setSource(event.state.type);
}
});
// If there's no current type set, assume video
if (!hasInitialType) {
currentType = 'video';
}
// Replace current history state
if (historySupport && types.includes(currentType)) {
window.history.replaceState({ type: currentType }, '', hasInitialType ? `#${currentType}` : '');
}
// If it's not video, load the source
if (currentType !== 'video') {
setSource(currentType, true);
}
render(currentType);
});
})();

81
demo/src/js/sources.js Normal file
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
demo/src/js/tab-focus.js Normal file
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);
});

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;

View File

@ -3,6 +3,9 @@
// ==========================================================================
@charset 'UTF-8';
@import '../../../../src/sass/lib/css-vars';
$css-vars-use-native: true;
// Settings
@import '../settings/breakpoints';
@import '../settings/colors';

View File

@ -5,79 +5,80 @@
// Shared
.button,
.button__count {
align-items: center;
background: $color-button-background;
border: 0;
border-radius: $border-radius-base;
box-shadow: 0 1px 1px rgba(#000, 0.1);
color: $color-button-text;
display: inline-flex;
padding: ($spacing-base * 0.75);
position: relative;
text-shadow: none;
user-select: none;
vertical-align: middle;
align-items: center;
border: 0;
border-radius: $border-radius-base;
box-shadow: 0 1px 1px rgba(#000, 0.1);
display: inline-flex;
padding: ($spacing-base * 0.75);
position: relative;
text-shadow: none;
user-select: none;
vertical-align: middle;
}
// Buttons
.button {
font-weight: $font-weight-bold;
padding-left: $spacing-base;
padding-right: $spacing-base;
transition: all 0.2s ease;
background: $color-button-background;
color: $color-button-text;
font-weight: $font-weight-bold;
padding-left: ($spacing-base * 1.25);
padding-right: ($spacing-base * 1.25);
transition: all 0.2s ease;
&:hover,
&:focus {
color: $gray-dark;
&:hover,
&:focus {
background: $color-button-background-hover;
// Remove the underline/border
&::after {
display: none;
}
// Remove the underline/border
&::after {
display: none;
}
}
&:hover {
box-shadow: 0 2px 2px rgba(#000, 0.1);
transform: translateY(-1px);
}
&:hover {
box-shadow: 0 2px 2px rgba(#000, 0.1);
}
&:focus {
outline: 0;
}
&:focus {
outline: 0;
}
&.tab-focus {
@include tab-focus();
}
&.tab-focus {
@include tab-focus();
}
&:active {
transform: translateY(1px);
}
&:active {
top: 1px;
}
}
// Button group
.button--with-count {
display: inline-flex;
display: inline-flex;
.button .icon {
flex-shrink: 0;
}
.button .icon {
flex-shrink: 0;
}
}
// Count bubble
.button__count {
animation: fadein 0.2s ease;
margin-left: ($spacing-base / 2);
animation: fadein 0.2s ease;
background: $color-button-count-background;
color: $color-button-count-text;
margin-left: ($spacing-base * 0.75);
&::before {
border: $arrow-size solid transparent;
border-left-width: 0;
border-right-color: $color-button-background;
content: '';
height: 0;
position: absolute;
right: 100%;
top: 50%;
transform: translateY(-50%);
width: 0;
}
&::before {
border: $arrow-size solid transparent;
border-left-width: 0;
border-right-color: $color-button-count-background;
content: '';
height: 0;
position: absolute;
right: 100%;
top: 50%;
transform: translateY(-50%);
width: 0;
}
}

View File

@ -3,17 +3,28 @@
// ==========================================================================
header {
padding-bottom: $spacing-base;
text-align: center;
padding-bottom: $spacing-base;
text-align: center;
.call-to-action {
margin-top: ($spacing-base * 1.5);
}
h1 span {
animation: 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;
}
@media only screen and (min-width: $screen-md) {
margin-right: ($spacing-base * 3);
max-width: 360px;
padding-bottom: ($spacing-base * 2);
text-align: left;
.call-to-action {
margin-top: ($spacing-base * 1.5);
}
@media only screen and (min-width: $screen-md) {
margin-right: ($spacing-base * 3);
max-width: 360px;
padding-bottom: ($spacing-base * 2);
text-align: left;
p:first-of-type {
@include font-size($font-size-base + 1);
}
}
}

View File

@ -4,20 +4,20 @@
// Base size icon styles
.icon {
fill: currentColor;
height: $icon-size;
vertical-align: -3px;
width: $icon-size;
fill: currentColor;
height: $icon-size;
vertical-align: -3px;
width: $icon-size;
}
// Within elements
a svg,
button svg,
label svg {
pointer-events: none;
pointer-events: none;
}
a .icon,
.btn .icon {
margin-right: floor($spacing-base / 3);
margin-right: ($spacing-base / 2);
}

View File

@ -4,46 +4,45 @@
// Make a <button> look like an <a>
button.faux-link {
@extend a; // stylelint-disable-line
@include cancel-button-styles();
@extend a; // stylelint-disable-line
@include cancel-button-styles();
}
// Links
a {
border-bottom: 1px dotted currentColor;
color: $color-link;
font-weight: $font-weight-bold;
position: relative;
text-decoration: none;
transition: all 0.2s ease;
border-bottom: 1px dotted currentColor;
color: $color-link;
position: relative;
text-decoration: none;
transition: all 0.2s ease;
&::after {
background: currentColor;
content: '';
height: 1px;
left: 50%;
position: absolute;
top: 100%;
transform: translateX(-50%);
transition: width 0.2s ease;
width: 0;
}
&:hover,
&:focus {
border-bottom-color: transparent;
outline: 0;
&::after {
background: currentColor;
content: '';
height: 1px;
left: 50%;
position: absolute;
top: 100%;
transform: translateX(-50%);
transition: width 0.2s ease;
width: 0;
width: 100%;
}
}
&:hover,
&:focus {
border-bottom-color: transparent;
outline: 0;
&.tab-focus {
@include tab-focus();
}
&::after {
width: 100%;
}
}
&.tab-focus {
@include tab-focus();
}
&.no-border::after {
display: none;
}
&.no-border::after {
display: none;
}
}

View File

@ -5,7 +5,7 @@
// Lists
ul,
li {
list-style: none;
margin: 0;
padding: 0;
list-style: none;
margin: 0;
padding: 0;
}

View File

@ -5,6 +5,6 @@
img,
video,
audio {
max-width: 100%;
vertical-align: middle;
max-width: 100%;
vertical-align: middle;
}

View File

@ -3,7 +3,7 @@
// ==========================================================================
nav {
display: flex;
justify-content: center;
margin-bottom: $spacing-base;
display: flex;
justify-content: center;
margin-bottom: $spacing-base;
}

View File

@ -2,49 +2,35 @@
// Examples
// ==========================================================================
// For non supported browsers
video {
max-width: 100%;
vertical-align: middle;
}
// Example players
.plyr {
border-radius: $border-radius-base;
box-shadow: 0 2px 5px rgba(#000, 0.2);
margin: $spacing-base auto;
border-radius: $border-radius-large;
box-shadow: 0 2px 15px rgba(#000, 0.1);
margin: $spacing-base auto;
&.plyr--audio {
max-width: 480px;
}
&.plyr--audio {
max-width: 480px;
}
}
.plyr__video-wrapper::after {
border: 1px solid rgba(#000, 0.15);
border-radius: inherit;
bottom: 0;
content: '';
left: 0;
pointer-events: none;
position: absolute;
right: 0;
top: 0;
z-index: 3;
border: 1px solid rgba(#000, 0.15);
border-radius: inherit;
bottom: 0;
content: '';
left: 0;
pointer-events: none;
position: absolute;
right: 0;
top: 0;
z-index: 3;
}
// Style full supported player
.plyr__cite {
display: none;
margin-top: $spacing-base;
color: $color-gray-500;
.icon {
margin-right: ceil($spacing-base / 6);
}
}
.plyr--video:not(.plyr--youtube):not(.plyr--vimeo) ~ ul .plyr__cite--video,
.plyr--audio ~ ul .plyr__cite--audio,
.plyr--youtube ~ ul .plyr__cite--youtube,
.plyr--vimeo ~ ul .plyr__cite--vimeo {
display: block;
.icon {
margin-right: ceil($spacing-base / 6);
}
}

View File

@ -4,61 +4,60 @@
html,
body {
display: flex;
width: 100%;
display: flex;
width: 100%;
}
html {
background: $page-background;
background-attachment: fixed;
height: 100%;
background: $page-background;
background-attachment: fixed;
height: 100%;
}
body {
align-items: center;
display: flex;
flex-direction: column;
min-height: 100%;
align-items: center;
display: flex;
flex-direction: column;
min-height: 100%;
}
.grid {
flex: 1;
overflow: auto;
flex: 1;
overflow: auto;
}
main {
margin: auto;
padding-bottom: 1px; // Collapsing margins
text-align: center;
margin: auto;
padding-bottom: 1px; // Collapsing margins
text-align: center;
}
aside {
align-items: center;
background: #fff;
color: $gray;
display: flex;
flex-shrink: 0;
justify-content: center;
padding: ($spacing-base * 0.75);
position: relative;
text-align: center;
text-shadow: none;
width: 100%;
align-items: center;
background: #fff;
display: flex;
flex-shrink: 0;
justify-content: center;
padding: $spacing-base;
position: relative;
text-align: center;
text-shadow: none;
width: 100%;
.icon {
fill: $color-twitter;
margin-right: ($spacing-base / 2);
}
p {
margin: 0;
}
a {
color: $color-twitter;
&.tab-focus {
@include tab-focus($color-twitter);
}
.icon {
fill: $color-twitter;
margin-right: ($spacing-base / 2);
}
p {
margin: 0;
}
a {
color: $color-twitter;
&.tab-focus {
@include tab-focus($color-twitter);
}
}
}

View File

@ -5,26 +5,26 @@
// Error page
html.error,
.error body {
height: 100%;
height: 100%;
}
html.error {
background: $page-background;
background-attachment: fixed;
background: $page-background;
background-attachment: fixed;
}
.error body {
align-items: center;
display: flex;
width: 100%;
align-items: center;
display: flex;
width: 100%;
}
.error main {
padding: $spacing-base;
text-align: center;
width: 100%;
padding: $spacing-base;
text-align: center;
width: 100%;
p {
@include font-size($font-size-large);
}
p {
@include font-size($font-size-large);
}
}

View File

@ -3,17 +3,17 @@
// ==========================================================================
.grid {
margin: 0 auto;
padding: $spacing-base;
margin: 0 auto;
padding: $spacing-base;
@media only screen and (min-width: $screen-md) {
align-items: center;
display: flex;
max-width: $container-max-width;
width: 100%;
@media only screen and (min-width: $screen-md) {
align-items: center;
display: flex;
max-width: $container-max-width;
width: 100%;
> * {
flex: 1;
}
> * {
flex: 1;
}
}
}

View File

@ -4,10 +4,24 @@
// Fade
@keyframes fadein {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes shrinkHide {
0% {
opacity: 0.5;
width: 38px;
}
20% {
width: 45px;
}
100% {
opacity: 0;
width: 0;
}
}

View File

@ -3,43 +3,46 @@
// ==========================================================================
@font-face {
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-light;
src: url('https://cdn.plyr.io/static/fonts/gordita-light.woff2') format('woff2'), url('https://cdn.plyr.io/static/fonts/gordita-light.woff') format('woff');
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-light;
src: url('https://cdn.plyr.io/static/fonts/gordita-light.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-light.woff') format('woff');
}
@font-face {
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-regular;
src: url('https://cdn.plyr.io/static/fonts/gordita-regular.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-regular.woff') format('woff');
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-regular;
src: url('https://cdn.plyr.io/static/fonts/gordita-regular.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-regular.woff') format('woff');
}
@font-face {
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-medium;
src: url('https://cdn.plyr.io/static/fonts/gordita-medium.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-medium.woff') format('woff');
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-medium;
src: url('https://cdn.plyr.io/static/fonts/gordita-medium.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-medium.woff') format('woff');
}
@font-face {
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-bold;
src: url('https://cdn.plyr.io/static/fonts/gordita-bold.woff2') format('woff2'), url('https://cdn.plyr.io/static/fonts/gordita-bold.woff') format('woff');
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-bold;
src: url('https://cdn.plyr.io/static/fonts/gordita-bold.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-bold.woff') format('woff');
}
@font-face {
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-black;
src: url('https://cdn.plyr.io/static/fonts/gordita-black.woff2') format('woff2'), url('https://cdn.plyr.io/static/fonts/gordita-black.woff') format('woff');
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-black;
src: url('https://cdn.plyr.io/static/fonts/gordita-black.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-black.woff') format('woff');
}

View File

@ -5,50 +5,50 @@
// Convert a <button> into an <a>
// ---------------------------------------
@mixin cancel-button-styles() {
background: transparent;
border: 0;
border-radius: 0;
cursor: pointer;
font: inherit;
line-height: $line-height-base;
margin: 0;
padding: 0;
position: relative;
text-align: inherit;
text-shadow: inherit;
-moz-user-select: text; // stylelint-disable-line
vertical-align: baseline;
width: auto;
background: transparent;
border: 0;
border-radius: 0;
cursor: pointer;
font: inherit;
line-height: $line-height-base;
margin: 0;
padding: 0;
position: relative;
text-align: inherit;
text-shadow: inherit;
-moz-user-select: text; // stylelint-disable-line
vertical-align: baseline;
width: auto;
}
// Nicer focus styles
// ---------------------------------------
@mixin tab-focus($color: $tab-focus-default-color) {
box-shadow: 0 0 0 3px rgba($color, 0.35);
outline: 0;
box-shadow: 0 0 0 3px rgba($color, 0.35);
outline: 0;
}
// Use rems for font sizing
// Leave <body> at 100%/16px
// ---------------------------------------
@function calculate-rem($size) {
$rem: $size / 16;
@return #{$rem}rem;
$rem: $size / 16;
@return #{$rem}rem;
}
@mixin font-size($size: 16) {
font-size: $size * 1px; // Fallback in px
font-size: calculate-rem($size);
@mixin font-size($size: $font-size-base) {
font-size: $size * 1px; // Fallback in px
font-size: calculate-rem($size);
}
// Font smoothing
// ---------------------------------------
@mixin font-smoothing($enabled: true) {
@if $enabled {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
} @else {
-moz-osx-font-smoothing: auto;
-webkit-font-smoothing: subpixel-antialiased;
}
@if $enabled {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
} @else {
-moz-osx-font-smoothing: auto;
-webkit-font-smoothing: subpixel-antialiased;
}
}

View File

@ -10,9 +10,9 @@
*/
html {
line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
@ -23,7 +23,7 @@ html {
*/
body {
margin: 0;
margin: 0;
}
/**
@ -36,7 +36,7 @@ footer,
header,
nav,
section {
display: block;
display: block;
}
/**
@ -45,8 +45,8 @@ section {
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
@ -60,8 +60,8 @@ h1 {
figcaption,
figure,
main {
/* 1 */
display: block;
/* 1 */
display: block;
}
/**
@ -69,7 +69,7 @@ main {
*/
figure {
margin: 1em 40px;
margin: 1em 40px;
}
/**
@ -78,9 +78,9 @@ figure {
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
@ -89,8 +89,8 @@ hr {
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
@ -102,8 +102,8 @@ pre {
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
@ -112,9 +112,9 @@ a {
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
@ -123,7 +123,7 @@ abbr[title] {
b,
strong {
font-weight: inherit;
font-weight: inherit;
}
/**
@ -132,7 +132,7 @@ strong {
b,
strong {
font-weight: bolder;
font-weight: bolder;
}
/**
@ -143,8 +143,8 @@ strong {
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
@ -152,7 +152,7 @@ samp {
*/
dfn {
font-style: italic;
font-style: italic;
}
/**
@ -160,8 +160,8 @@ dfn {
*/
mark {
background-color: #ff0;
color: #000;
background-color: #ff0;
color: #000;
}
/**
@ -169,7 +169,7 @@ mark {
*/
small {
font-size: 80%;
font-size: 80%;
}
/**
@ -179,18 +179,18 @@ small {
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
bottom: -0.25em;
}
sup {
top: -0.5em;
top: -0.5em;
}
/* Embedded content
@ -202,7 +202,7 @@ sup {
audio,
video {
display: inline-block;
display: inline-block;
}
/**
@ -210,8 +210,8 @@ video {
*/
audio:not([controls]) {
display: none;
height: 0;
display: none;
height: 0;
}
/**
@ -219,7 +219,7 @@ audio:not([controls]) {
*/
img {
border-style: none;
border-style: none;
}
/**
@ -227,7 +227,7 @@ img {
*/
svg:not(:root) {
overflow: hidden;
overflow: hidden;
}
/* Forms
@ -243,10 +243,10 @@ input,
optgroup,
select,
textarea {
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
@ -256,8 +256,8 @@ textarea {
button,
input {
/* 1 */
overflow: visible;
/* 1 */
overflow: visible;
}
/**
@ -267,8 +267,8 @@ input {
button,
select {
/* 1 */
text-transform: none;
/* 1 */
text-transform: none;
}
/**
@ -281,7 +281,7 @@ button,
html [type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button; /* 2 */
-webkit-appearance: button; /* 2 */
}
/**
@ -292,8 +292,8 @@ button::-moz-focus-inner,
[type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner {
border-style: none;
padding: 0;
border-style: none;
padding: 0;
}
/**
@ -304,7 +304,7 @@ button:-moz-focusring,
[type='button']:-moz-focusring,
[type='reset']:-moz-focusring,
[type='submit']:-moz-focusring {
outline: 1px dotted ButtonText;
outline: 1px dotted ButtonText;
}
/**
@ -312,7 +312,7 @@ button:-moz-focusring,
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
padding: 0.35em 0.75em 0.625em;
}
/**
@ -323,12 +323,12 @@ fieldset {
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
@ -337,8 +337,8 @@ legend {
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
@ -346,7 +346,7 @@ progress {
*/
textarea {
overflow: auto;
overflow: auto;
}
/**
@ -356,8 +356,8 @@ textarea {
[type='checkbox'],
[type='radio'] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
@ -366,7 +366,7 @@ textarea {
[type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button {
height: auto;
height: auto;
}
/**
@ -375,8 +375,8 @@ textarea {
*/
[type='search'] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
@ -385,7 +385,7 @@ textarea {
[type='search']::-webkit-search-cancel-button,
[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
-webkit-appearance: none;
}
/**
@ -394,8 +394,8 @@ textarea {
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
@ -408,7 +408,7 @@ textarea {
details,
menu {
display: block;
display: block;
}
/*
@ -416,7 +416,7 @@ menu {
*/
summary {
display: list-item;
display: list-item;
}
/* Scripting
@ -427,7 +427,7 @@ summary {
*/
canvas {
display: inline-block;
display: inline-block;
}
/**
@ -435,7 +435,7 @@ canvas {
*/
template {
display: none;
display: none;
}
/* Hidden
@ -446,5 +446,5 @@ template {
*/
[hidden] {
display: none;
display: none;
}

View File

@ -7,5 +7,5 @@
*,
*::after,
*::before {
box-sizing: border-box;
box-sizing: border-box;
}

View File

@ -2,31 +2,41 @@
// Colors
// ==========================================================================
// Greyscale
$gray-dark: #343f4a;
$gray: #55646b;
$gray-light: #cbd0d3;
$gray-lighter: #dbe3e8;
$off-white: #f2f5f7;
// Grayscale
$color-gray-900: hsl(210, 15%, 16%);
$color-gray-800: lighten($color-gray-900, 9%);
$color-gray-700: lighten($color-gray-800, 9%);
$color-gray-600: lighten($color-gray-700, 9%);
$color-gray-500: lighten($color-gray-600, 9%);
$color-gray-400: lighten($color-gray-500, 9%);
$color-gray-300: lighten($color-gray-400, 9%);
$color-gray-200: lighten($color-gray-300, 9%);
$color-gray-100: lighten($color-gray-200, 9%);
$color-gray-50: lighten($color-gray-100, 9%);
// Branding
$color-brand-primary: hsl(198, 100%, 50%);
// Text
$color-text: #fff;
// Plyr
$color-brand-primary: #1aafff;
$color-text: $color-gray-700;
$color-headings: $color-brand-primary;
// Brands
$color-twitter: #4baaf4;
$color-youtube: #cc181e;
$color-vimeo: #19b7ed;
// Elements
$color-link: #fff;
$color-background: $color-brand-primary;
$color-link: $color-brand-primary;
// Background
$color-background-from: hsl(198, 100%, 94%);
$color-background-to: hsl(198, 100%, 98%);
// Buttons
$color-button-background: #fff;
$color-button-text: $gray;
$color-button-background: $color-brand-primary;
$color-button-text: #fff;
$color-button-background-hover: hsl(198, 100%, 55%);
$color-button-count-background: #fff;
$color-button-count-text: $color-gray-600;
// Focus
$tab-focus-default-color: #fff;

View File

@ -7,6 +7,7 @@ $arrow-size: 5px;
// Radii
$border-radius-base: 4px;
$border-radius-large: 8px;
// Background
$page-background: linear-gradient(to left top, lighten($color-background, 10%), darken($color-background, 20%));
$page-background: linear-gradient(to left top, $color-background-from, $color-background-to);

View File

@ -2,4 +2,4 @@
// Layout
// ==========================================================================
$container-max-width: 1260px;
$container-max-width: 1240px;

View File

@ -2,18 +2,17 @@
// Plyr Settings
// ==========================================================================
// Font
$plyr-font-family: inherit;
// Sizes
$plyr-font-size-base: 13px;
$plyr-font-size-small: 12px;
$plyr-font-size-time: 11px;
$plyr-font-size-badges: 9px;
// Captions
$plyr-font-size-captions-base: $plyr-font-size-base;
$plyr-font-size-captions-small: $plyr-font-size-small;
$plyr-font-size-captions-medium: 18px;
$plyr-font-size-captions-large: 21px;
$plyr-font-size-menu: $plyr-font-size-base;
@include css-vars(
(
--plyr-color-main: $color-brand-primary,
--plyr-font-size-base: 13px,
--plyr-font-size-small: 12px,
--plyr-font-size-time: 11px,
--plyr-font-size-badges: 9px,
--plyr-font-size-menu: var(--plyr-font-size-base),
--plyr-font-weight-regular: 500,
--plyr-font-weight-bold: 600,
--plyr-font-size-captions-medium: 18px,
--plyr-font-size-captions-large: 21px,
)
);

View File

@ -2,4 +2,4 @@
// Colors
// ==========================================================================
$spacing-base: 20px;
$spacing-base: 16px;

View File

@ -3,7 +3,7 @@
// ==========================================================================
$font-sans-serif: 'Gordita', 'Avenir', 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol';
'Segoe UI Symbol';
$font-size-base: 15;
$font-size-small: 13;

View File

@ -4,32 +4,31 @@
// Set to 100% for rem sizing
html {
font-size: 100%;
font-size: 100%;
}
body {
@include font-smoothing();
@include font-size($font-size-base);
color: $color-text;
font-family: $font-sans-serif;
font-weight: $font-weight-medium;
line-height: $line-height-base;
text-shadow: 0 1px 1px rgba(#000, 0.15);
@include font-smoothing();
@include font-size($font-size-base);
color: $color-text;
font-family: $font-sans-serif;
font-weight: $font-weight-medium;
line-height: $line-height-base;
}
button,
input,
select,
textarea {
font: inherit;
font: inherit;
}
p,
small {
margin: 0 0 $spacing-base;
margin: 0 0 ($spacing-base * 1.5);
}
small {
@include font-size($font-size-small);
display: block;
@include font-size($font-size-small);
display: block;
}

View File

@ -3,9 +3,10 @@
// ==========================================================================
h1 {
@include font-size($font-size-h1);
font-weight: $font-weight-bold;
letter-spacing: $letter-spacing-headings;
line-height: 1.2;
margin: 0 0 $spacing-base;
@include font-size($font-size-h1);
color: $color-headings;
font-weight: $font-weight-bold;
letter-spacing: $letter-spacing-headings;
line-height: 1.2;
margin: 0 0 ($spacing-base * 1.5);
}

View File

@ -3,5 +3,5 @@
// ==========================================================================
.no-border {
border: 0;
border: 0;
}

View File

@ -3,18 +3,18 @@
// ==========================================================================
[hidden] {
display: none;
display: none;
}
// Hide only visually, but have it available for screen readers: h5bp.com/v
.sr-only {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
opacity: 0.001;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
opacity: 0.001;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}

80
demo/yarn.lock Normal file
View File

@ -0,0 +1,80 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@sentry/browser@^5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.22.3.tgz#7a64bd1cf01bf393741a3e4bf35f82aa927f5b4e"
integrity sha512-2TzE/CoBa5ZkvxJizDdi1Iz1ldmXSJpFQ1mL07PIXBjCt0Wxf+WOuFSj5IP4L40XHfJE5gU8wEvSH0VDR8nXtA==
dependencies:
"@sentry/core" "5.22.3"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/core@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.22.3.tgz#030f435f2b518f282ba8bd954dac90cd70888bd7"
integrity sha512-eGL5uUarw3o4i9QUb9JoFHnhriPpWCaqeaIBB06HUpdcvhrjoowcKZj1+WPec5lFg5XusE35vez7z/FPzmJUDw==
dependencies:
"@sentry/hub" "5.22.3"
"@sentry/minimal" "5.22.3"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/hub@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.22.3.tgz#08309a70d2ea8d5e313d05840c1711f34f2fffe5"
integrity sha512-INo47m6N5HFEs/7GMP9cqxOIt7rmRxdERunA3H2L37owjcr77MwHVeeJ9yawRS6FMtbWXplgWTyTIWIYOuqVbw==
dependencies:
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/minimal@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.22.3.tgz#706e4029ae5494123d3875c658ba8911aa5cc440"
integrity sha512-HoINpYnVYCpNjn2XIPIlqH5o4BAITpTljXjtAftOx6Hzj+Opjg8tR8PWliyKDvkXPpc4kXK9D6TpEDw8MO0wZA==
dependencies:
"@sentry/hub" "5.22.3"
"@sentry/types" "5.22.3"
tslib "^1.9.3"
"@sentry/types@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.22.3.tgz#d1d547b30ee8bd7771fa893af74c4f3d71f0fd18"
integrity sha512-cv+VWK0YFgCVDvD1/HrrBWOWYG3MLuCUJRBTkV/Opdy7nkdNjhCAJQrEyMM9zX0sac8FKWKOHT0sykNh8KgmYw==
"@sentry/utils@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.22.3.tgz#e3bda3e789239eb16d436f768daa12829f33d18f"
integrity sha512-AHNryXMBvIkIE+GQxTlmhBXD0Ksh+5w1SwM5qi6AttH+1qjWLvV6WB4+4pvVvEoS8t5F+WaVUZPQLmCCWp6zKw==
dependencies:
"@sentry/types" "5.22.3"
tslib "^1.9.3"
core-js@^3.6.5:
version "3.6.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
custom-event-polyfill@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee"
integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
shr-buttons@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/shr-buttons/-/shr-buttons-2.0.3.tgz#2ffd021fc3d789e1510ce2736b938bd09ea1da5a"
integrity sha512-sPAgHiw4uaIt9TnxTfyZEedDChcldSVtnBHE44cpe/mSC7rqm4IEKZRLYqnVlTcGM+FSDNBPUNpSf50Q2ntd+w==
tslib@^1.9.3:
version "1.11.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
url-polyfill@^1.1.10:
version "1.1.10"
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.10.tgz#fd5bbcf66c9491fa682b0cb6d6a855e1643a9281"
integrity sha512-vSaPpaRgBrf41+Uky1myiSh6gpcbw8FpwHYnEy0abxndojOBnIs+yh/49gKYFLtUMP9qoNWjn6j9aUVy23Ie2A==

12
deploy.json Normal file
View File

@ -0,0 +1,12 @@
{
"cdn": {
"bucket": "plyr",
"domain": "cdn.plyr.io",
"region": "us-east-1"
},
"demo": {
"bucket": "plyr.io",
"domain": "plyr.io",
"region": "us-west-1"
}
}

BIN
dist/blank.mp4 vendored

Binary file not shown.

1
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

7701
dist/plyr.js vendored

File diff suppressed because it is too large Load Diff

1
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

10393
dist/plyr.polyfilled.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/plyr.svg vendored

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,458 +1,8 @@
// ==========================================================================
// Gulp build script
// ==========================================================================
/* global require, __dirname */
/* eslint no-console: "off" */
const del = require('del');
const path = require('path');
const gulp = require('gulp');
const gutil = require('gulp-util');
const concat = require('gulp-concat');
const filter = require('gulp-filter');
const sass = require('gulp-sass');
const cleancss = require('gulp-clean-css');
const run = require('run-sequence');
const header = require('gulp-header');
const prefix = require('gulp-autoprefixer');
const gitbranch = require('git-branch');
const svgstore = require('gulp-svgstore');
const svgmin = require('gulp-svgmin');
const rename = require('gulp-rename');
const s3 = require('gulp-s3');
const replace = require('gulp-replace');
const open = require('gulp-open');
const size = require('gulp-size');
const rollup = require('gulp-better-rollup');
const babel = require('rollup-plugin-babel');
const sourcemaps = require('gulp-sourcemaps');
const uglify = require('gulp-uglify-es').default;
const commonjs = require('rollup-plugin-commonjs');
const resolve = require('rollup-plugin-node-resolve');
const FastlyPurge = require('fastly-purge');
const through = require('through2');
const HubRegistry = require('gulp-hub');
const bundles = require('./bundles.json');
const pkg = require('./package.json');
const minSuffix = '.min';
// Paths
const root = __dirname;
const paths = {
plyr: {
// Source paths
src: {
sass: path.join(root, 'src/sass/**/*.scss'),
js: path.join(root, 'src/js/**/*.js'),
sprite: path.join(root, 'src/sprite/*.svg'),
},
// Output paths
output: path.join(root, 'dist/'),
},
demo: {
// Source paths
src: {
sass: path.join(root, 'demo/src/sass/**/*.scss'),
js: path.join(root, 'demo/src/js/**/*.js'),
},
// Output paths
output: path.join(root, 'demo/dist/'),
// Demo
root: path.join(root, 'demo/'),
},
upload: [
path.join(root, `dist/*${minSuffix}.*`),
path.join(root, 'dist/*.css'),
path.join(root, 'dist/*.svg'),
path.join(root, `demo/dist/*${minSuffix}.*`),
path.join(root, 'demo/dist/*.css'),
],
};
// Task arrays
const tasks = {
sass: [],
js: [],
sprite: [],
clean: ['clean'],
};
// Size plugin
const sizeOptions = { showFiles: true, gzip: true };
// Browserlist
const browsers = ['> 1%'];
// Babel config
const babelrc = (polyfill = false) => ({
presets: [
[
'@babel/preset-env',
{
targets: {
browsers,
},
useBuiltIns: polyfill ? 'usage' : false,
modules: false,
},
],
],
babelrc: false,
exclude: 'node_modules/**',
});
// Clean out /dist
gulp.task('clean', () => {
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);
});
const build = {
js(files, bundle, options) {
Object.keys(files).forEach(key => {
const name = `js:${key}`;
tasks.js.push(name);
const { output } = paths[bundle];
const polyfill = name.includes('polyfilled');
return gulp.task(name, () =>
gulp
.src(bundles[bundle].js[key])
.pipe(sourcemaps.init())
.pipe(concat(key))
.pipe(
rollup(
{
plugins: [resolve(), commonjs(), babel(babelrc(polyfill))],
},
options,
),
)
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(output))
.pipe(filter('**/*.js'))
.pipe(uglify())
.pipe(size(sizeOptions))
.pipe(rename({ suffix: minSuffix }))
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(output)),
);
});
},
sass(files, bundle) {
Object.keys(files).forEach(key => {
const name = `sass:${key}`;
tasks.sass.push(name);
return gulp.task(name, () =>
gulp
.src(bundles[bundle].sass[key])
.pipe(sass())
.on('error', gutil.log)
.pipe(concat(key))
.pipe(prefix(browsers, { cascade: false }))
.pipe(cleancss())
.pipe(size(sizeOptions))
.pipe(gulp.dest(paths[bundle].output)),
);
});
},
sprite(bundle) {
const name = `svg:sprite:${bundle}`;
tasks.sprite.push(name);
// Process Icons
return gulp.task(name, () =>
gulp
.src(paths[bundle].src.sprite)
.pipe(
svgmin({
plugins: [
{
removeDesc: true,
},
],
}),
)
.pipe(svgstore())
.pipe(rename({ basename: bundle }))
.pipe(size(sizeOptions))
.pipe(gulp.dest(paths[bundle].output)),
);
},
};
// Plyr core files
build.js(bundles.plyr.js, 'plyr', { name: 'Plyr', format: 'umd' });
build.sass(bundles.plyr.sass, 'plyr');
build.sprite('plyr');
// Demo files
build.sass(bundles.demo.sass, 'demo');
build.js(bundles.demo.js, 'demo', { format: 'iife' });
// Build all JS
gulp.task('js', () => {
run(tasks.js);
});
// Watch for file changes
gulp.task('watch', () => {
// Plyr core
gulp.watch(paths.plyr.src.js, tasks.js);
gulp.watch(paths.plyr.src.sass, tasks.sass);
gulp.watch(paths.plyr.src.sprite, tasks.sprite);
// Demo
gulp.watch(paths.demo.src.js, tasks.js);
gulp.watch(paths.demo.src.sass, tasks.sass);
});
// Build distribution
gulp.task('build', () => {
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite);
});
// Default gulp task
gulp.task('default', () => {
run('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
}
// If deployment is setup
if (Object.keys(credentials).includes('aws') && Object.keys(credentials).includes('fastly')) {
const { version } = pkg;
const { aws, fastly } = credentials;
// Get branch info
const branch = {
current: gitbranch.sync(),
master: 'master',
develop: 'develop',
};
const maxAge = 31536000; // 1 year
const options = {
cdn: {
headers: {
'Cache-Control': `max-age=${maxAge}`,
Vary: 'Accept-Encoding',
},
},
demo: {
uploadPath: branch.current === branch.develop ? 'beta/' : null,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
Vary: 'Accept-Encoding',
},
},
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://${aws.cdn.domain}/${version}`;
const cdnpath = new RegExp(`${aws.cdn.domain}/${regex}/`, 'gi');
const renameFile = rename(p => {
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
});
// Check we're on the correct branch to deploy
const canDeploy = () => {
const allowed = [branch.master, branch.develop];
if (!allowed.includes(branch.current)) {
console.error(`Must be on ${allowed.join(', ')} to publish! (current: ${branch.current})`);
return false;
}
return true;
};
gulp.task('version', () => {
if (!canDeploy()) {
return null;
}
console.log(`Updating versions to '${version}'...`);
// Replace versioned URLs in source
const files = ['plyr.js', 'plyr.polyfilled.js', 'defaults.js'];
return gulp
.src(files.map(file => path.join(root, `src/js/${file}`)))
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest(path.join(root, 'src/js/')));
});
// Publish version to CDN bucket
gulp.task('cdn', () => {
if (!canDeploy()) {
return null;
}
console.log(`Uploading '${version}' to ${aws.cdn.domain}...`);
// Upload to CDN
return (
gulp
.src(paths.upload)
.pipe(renameFile)
// Remove min suffix from source map URL
.pipe(
replace(
/sourceMappingURL=([\w-?.]+)/,
(match, p1) => `sourceMappingURL=${p1.replace(minSuffix, '')}`,
),
)
.pipe(
size({
showFiles: true,
gzip: true,
}),
)
.pipe(replace(localPath, versionPath))
.pipe(s3(aws.cdn, options.cdn))
);
});
// Purge the fastly cache incase any 403/404 are cached
gulp.task('purge', () => {
const list = [];
return gulp
.src(paths.upload)
.pipe(
through.obj((file, enc, cb) => {
const filename = file.path.split('/').pop();
list.push(`${versionPath}/${filename}`);
cb(null);
}),
)
.on('end', () => {
const purge = new FastlyPurge(fastly.token);
list.forEach(url => {
console.log(`Purging ${url}...`);
purge.url(url, (error, result) => {
if (error) {
console.log(error);
} else if (result) {
console.log(result);
}
});
});
});
});
// Publish to demo bucket
gulp.task('demo', () => {
if (!canDeploy()) {
return null;
}
console.log(`Uploading '${version}' demo to ${aws.demo.domain}...`);
// Replace versioned files in readme.md
gulp.src([`${root}/readme.md`])
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest(root));
// 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);
}
gulp.src(pages)
.pipe(replace(localPath, versionPath))
.pipe(s3(aws.demo, options.demo));
// Only update CDN for master (prod)
if (branch.current !== branch.master) {
return null;
}
// Upload error.html to cdn (as well as demo site)
return gulp
.src([error])
.pipe(replace(localPath, versionPath))
.pipe(s3(aws.cdn, options.demo));
});
// Update symlinks for latest
/* gulp.task("symlinks", function () {
console.log("Updating symlinks...");
return gulp.src(paths.upload)
.pipe(through.obj(function (chunk, enc, callback) {
if (chunk.stat.isFile()) {
// Get the filename
var filename = chunk.path.split("/").reverse()[0];
// Create the 0 byte redirect files to upload
createFile(filename, "")
.pipe(rename(function (path) {
path.dirname = path.dirname.replace(".", "latest");
}))
// Upload to S3 with correct headers
.pipe(s3(aws.cdn, options.symlinks(version, filename)));
}
callback(null, chunk);
}));
}); */
// Open the demo site to check it's ok
gulp.task('open', callback => {
gulp.src(__filename).pipe(
open({
uri: `https://${aws.demo.domain}`,
}),
);
callback();
});
// Do everything
gulp.task('deploy', () =>
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'purge', 'demo', 'open'),
);
}
gulp.registry(new HubRegistry(['tasks/*.js']));

View File

@ -1,92 +1,99 @@
{
"name": "plyr",
"version": "3.4.5",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>",
"keywords": [
"HTML5 Video",
"HTML5 Audio",
"Media Player",
"DASH",
"Shaka",
"WordPress",
"HLS"
],
"main": "./dist/plyr.js",
"browser": "./dist/plyr.min.js",
"sass": "./src/sass/plyr.scss",
"style": "./dist/plyr.css",
"license": "MIT",
"repository": {
"type": "git",
"url": "git://github.com/sampotts/plyr.git"
},
"bugs": {
"url": "https://github.com/sampotts/plyr/issues"
},
"directories": {
"doc": "readme.md"
},
"scripts": {
"build": "gulp build",
"lint": "eslint src/js && npm run-script remark",
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"babel-core": "^6.26.3",
"babel-eslint": "^10.0.1",
"@babel/preset-env": "^7.1.0",
"del": "^3.0.0",
"eslint": "^5.7.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^3.1.0",
"eslint-plugin-import": "^2.14.0",
"fastly-purge": "^1.0.1",
"git-branch": "^2.0.1",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^6.0.0",
"gulp-better-rollup": "^3.3.0",
"gulp-clean-css": "^3.10.0",
"gulp-concat": "^2.6.1",
"gulp-filter": "^5.1.0",
"gulp-header": "^2.0.5",
"gulp-open": "^3.0.1",
"gulp-postcss": "^8.0.0",
"gulp-rename": "^1.4.0",
"gulp-replace": "^1.0.0",
"gulp-s3": "^0.11.0",
"gulp-sass": "^4.0.2",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.4",
"gulp-svgmin": "^2.1.0",
"gulp-svgstore": "^7.0.0",
"gulp-uglify-es": "^1.0.4",
"gulp-util": "^3.0.8",
"postcss-custom-properties": "^8.0.8",
"prettier-eslint": "^8.8.2",
"prettier-stylelint": "^0.4.2",
"remark-cli": "^5.0.0",
"remark-validate-links": "^7.1.0",
"rollup-plugin-babel": "^4.0.3",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-node-resolve": "^3.4.0",
"run-sequence": "^2.2.1",
"stylelint": "^9.6.0",
"stylelint-config-prettier": "^4.0.0",
"stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.2.0",
"stylelint-order": "^1.0.0",
"stylelint-scss": "^3.3.2",
"stylelint-selector-bem-pattern": "^2.0.0",
"through2": "^2.0.3"
},
"dependencies": {
"core-js": "^2.5.7",
"custom-event-polyfill": "^1.0.6",
"loadjs": "^3.5.4",
"raven-js": "^3.27.0",
"url-polyfill": "^1.1.0"
}
"name": "plyr",
"version": "3.6.7",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>",
"main": "dist/plyr.js",
"types": "src/js/plyr.d.ts",
"module": "dist/plyr.min.mjs",
"jsnext:main": "dist/plyr.min.mjs",
"browser": "dist/plyr.min.js",
"sass": "src/sass/plyr.scss",
"style": "dist/plyr.css",
"keywords": [
"HTML5 Video",
"HTML5 Audio",
"Media Player",
"DASH",
"Shaka",
"WordPress",
"HLS"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "git://github.com/sampotts/plyr.git"
},
"bugs": {
"url": "https://github.com/sampotts/plyr/issues"
},
"browserslist": "> 1%",
"scripts": {
"build": "gulp build",
"lint": "eslint src/js && npm run-script remark",
"lint:fix": "eslint --fix src/js",
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
"deploy": "yarn lint && gulp version && gulp build && gulp deploy",
"format": "prettier --write \"./{src,demo/src}/**/*.{js,scss}\"",
"start": "gulp"
},
"devDependencies": {
"@sampotts/eslint-config": "1.1.7",
"autoprefixer": "^10.2.5",
"aws-sdk": "^2.888.0",
"@babel/core": "^7.13.15",
"@babel/preset-env": "^7.13.15",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"babel-eslint": "^10.1.0",
"browser-sync": "^2.26.14",
"colorette": "1.2.2",
"cssnano": "^5.0.1",
"del": "^6.0.0",
"eslint": "^7.23.0",
"fancy-log": "^1.3.3",
"git-branch": "^2.0.1",
"gulp": "^4.0.2",
"gulp-awspublish": "^4.1.2",
"gulp-better-rollup": "^4.0.1",
"gulp-filter": "^6.0.0",
"gulp-header": "^2.0.9",
"gulp-hub": "^4.2.0",
"gulp-if": "^3.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-open": "^3.0.1",
"gulp-plumber": "^1.2.1",
"gulp-postcss": "^9.0.0",
"gulp-rename": "^2.0.0",
"gulp-replace": "^1.0.0",
"gulp-sass": "^4.1.0",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-svgstore": "^7.0.1",
"gulp-terser": "^2.0.1",
"postcss": "^8.2.10",
"postcss-custom-properties": "^11.0.0",
"prettier-eslint": "^12.0.0",
"prettier-stylelint": "^0.4.2",
"remark-cli": "^9.0.0",
"remark-validate-links": "^10.0.4",
"rollup": "^2.45.2",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"stylelint": "^13.12.0",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-recommended": "^4.0.0",
"stylelint-config-sass-guidelines": "^8.0.0",
"stylelint-order": "^4.1.0",
"stylelint-scss": "^3.19.0",
"stylelint-selector-bem-pattern": "^2.1.0"
},
"dependencies": {
"core-js": "^3.10.1",
"custom-event-polyfill": "^1.0.7",
"loadjs": "^4.2.0",
"rangetouch": "^2.0.1",
"url-polyfill": "^1.1.12"
}
}

View File

@ -1,32 +1,37 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
// Exclude from the editor
"files.exclude": {
"**/node_modules": true
},
// Exclude from search
"search.exclude": {
"dist/": true,
"demo/dist/": true
},
// Linting
"stylelint.enable": true,
"css.validate": false,
"scss.validate": false,
"javascript.validate.enable": false,
// Prettier
"prettier.eslintIntegration": true,
"prettier.stylelintIntegration": true,
// Formatting
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
// Trim on save
"files.trimTrailingWhitespace": true
}
}
"folders": [
{
"path": "."
}
],
"settings": {
"search.exclude": {
"**/node_modules": true,
"**/dist": true
},
// Linting
"stylelint.enable": true,
"css.validate": false,
"scss.validate": false,
"javascript.validate.enable": false,
// Formatting
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
// Trim on save
"files.trimTrailingWhitespace": true,
// Special file associations
"files.associations": {
".eslintrc": "jsonc"
},
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}
}

View File

@ -8,12 +8,12 @@ import support from './support';
import { dedupe } from './utils/arrays';
import browser from './utils/browser';
import {
createElement,
emptyElement,
getAttributesFromSelector,
insertAfter,
removeElement,
toggleClass,
createElement,
emptyElement,
getAttributesFromSelector,
insertAfter,
removeElement,
toggleClass,
} from './utils/elements';
import { on, triggerEvent } from './utils/events';
import fetch from './utils/fetch';
@ -23,363 +23,385 @@ import { getHTML } from './utils/strings';
import { parseUrl } from './utils/urls';
const captions = {
// Setup captions
setup() {
// Requires UI support
if (!this.supported.ui) {
return;
}
// Setup captions
setup() {
// Requires UI support
if (!this.supported.ui) {
return;
}
// Only Vimeo and HTML5 video supported at this point
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
// Clear menu and hide
if (
is.array(this.config.controls) &&
this.config.controls.includes('settings') &&
this.config.settings.includes('captions')
) {
controls.setCaptionsMenu.call(this);
}
// Only Vimeo and HTML5 video supported at this point
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
// Clear menu and hide
if (
is.array(this.config.controls) &&
this.config.controls.includes('settings') &&
this.config.settings.includes('captions')
) {
controls.setCaptionsMenu.call(this);
}
return;
}
return;
}
// Inject the container
if (!is.element(this.elements.captions)) {
this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
// Inject the container
if (!is.element(this.elements.captions)) {
this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
insertAfter(this.elements.captions, this.elements.wrapper);
}
insertAfter(this.elements.captions, this.elements.wrapper);
}
// Fix IE captions if CORS is used
// Fetch captions and inject as blobs instead (data URIs not supported!)
if (browser.isIE && window.URL) {
const elements = this.media.querySelectorAll('track');
// Fix IE captions if CORS is used
// Fetch captions and inject as blobs instead (data URIs not supported!)
if (browser.isIE && window.URL) {
const elements = this.media.querySelectorAll('track');
Array.from(elements).forEach(track => {
const src = track.getAttribute('src');
const url = parseUrl(src);
Array.from(elements).forEach((track) => {
const src = track.getAttribute('src');
const url = parseUrl(src);
if (
url !== null &&
url.hostname !== window.location.href.hostname &&
['http:', 'https:'].includes(url.protocol)
) {
fetch(src, 'blob')
.then(blob => {
track.setAttribute('src', window.URL.createObjectURL(blob));
})
.catch(() => {
removeElement(track);
});
}
if (
url !== null &&
url.hostname !== window.location.href.hostname &&
['http:', 'https:'].includes(url.protocol)
) {
fetch(src, 'blob')
.then((blob) => {
track.setAttribute('src', window.URL.createObjectURL(blob));
})
.catch(() => {
removeElement(track);
});
}
});
}
// Get and set initial data
// The "preferred" options are not realized unless / until the wanted language has a match
// * languages: Array of user's browser languages.
// * language: The language preferred by user settings or config
// * active: The state preferred by user settings or config
// * toggled: The real captions state
// Get and set initial data
// The "preferred" options are not realized unless / until the wanted language has a match
// * languages: Array of user's browser languages.
// * language: The language preferred by user settings or config
// * active: The state preferred by user settings or config
// * toggled: The real captions state
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
const languages = dedupe(browserLanguages.map((language) => language.split('-')[0]));
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
// Use first browser language when language is 'auto'
if (language === 'auto') {
[language] = languages;
}
// Use first browser language when language is 'auto'
if (language === 'auto') {
[language] = languages;
}
let active = this.storage.get('captions');
if (!is.boolean(active)) {
({ active } = this.config.captions);
}
let active = this.storage.get('captions');
if (!is.boolean(active)) {
({ active } = this.config.captions);
}
Object.assign(this.captions, {
toggled: false,
active,
language,
languages,
});
Object.assign(this.captions, {
toggled: false,
active,
language,
languages,
// Watch changes to textTracks and update captions menu
if (this.isHTML5) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
}
// Update available languages in list next tick (the event must not be triggered before the listeners)
setTimeout(captions.update.bind(this), 0);
},
// Update available language options in settings based on tracks
update() {
const tracks = captions.getTracks.call(this, true);
// Get the wanted language
const { active, language, meta, currentTrackNode } = this.captions;
const languageExists = Boolean(tracks.find((track) => track.language === language));
// Handle tracks (add event listener and "pseudo"-default)
if (this.isHTML5 && this.isVideo) {
tracks
.filter((track) => !meta.get(track))
.forEach((track) => {
this.debug.log('Track added', track);
// Attempt to store if the original dom element was "default"
meta.set(track, {
default: track.mode === 'showing',
});
// Turn off native caption rendering to avoid double captions
// Note: mode='hidden' forces a track to download. To ensure every track
// isn't downloaded at once, only 'showing' tracks should be reassigned
// eslint-disable-next-line no-param-reassign
if (track.mode === 'showing') {
// eslint-disable-next-line no-param-reassign
track.mode = 'hidden';
}
// Add event listener for cue changes
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
});
}
// Watch changes to textTracks and update captions menu
if (this.isHTML5) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
}
// Update language first time it matches, or if the previous matching track was removed
if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
captions.setLanguage.call(this, language);
captions.toggle.call(this, active && languageExists);
}
// Update available languages in list next tick (the event must not be triggered before the listeners)
setTimeout(captions.update.bind(this), 0);
},
// Enable or disable captions based on track length
toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
// Update available language options in settings based on tracks
update() {
const tracks = captions.getTracks.call(this, true);
// Get the wanted language
const { active, language, meta, currentTrackNode } = this.captions;
const languageExists = Boolean(tracks.find(track => track.language === language));
// Update available languages in list
if (
is.array(this.config.controls) &&
this.config.controls.includes('settings') &&
this.config.settings.includes('captions')
) {
controls.setCaptionsMenu.call(this);
}
},
// Handle tracks (add event listener and "pseudo"-default)
if (this.isHTML5 && this.isVideo) {
tracks.filter(track => !meta.get(track)).forEach(track => {
this.debug.log('Track added', track);
// Attempt to store if the original dom element was "default"
meta.set(track, {
default: track.mode === 'showing',
});
// Toggle captions display
// Used internally for the toggleCaptions method, with the passive option forced to false
toggle(input, passive = true) {
// If there's no full support
if (!this.supported.ui) {
return;
}
// Turn off native caption rendering to avoid double captions
track.mode = 'hidden';
const { toggled } = this.captions; // Current state
const activeClass = this.config.classNames.captions.active;
// Get the next state
// If the method is called without parameter, toggle based on current value
const active = is.nullOrUndefined(input) ? !toggled : input;
// Add event listener for cue changes
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
});
}
// Update state and trigger event
if (active !== toggled) {
// When passive, don't override user preferences
if (!passive) {
this.captions.active = active;
this.storage.set({ captions: active });
}
// Update language first time it matches, or if the previous matching track was removed
if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
captions.setLanguage.call(this, language);
captions.toggle.call(this, active && languageExists);
}
// Enable or disable captions based on track length
toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
// Update available languages in list
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
controls.setCaptionsMenu.call(this);
}
},
// Toggle captions display
// Used internally for the toggleCaptions method, with the passive option forced to false
toggle(input, passive = true) {
// If there's no full support
if (!this.supported.ui) {
return;
}
const { toggled } = this.captions; // Current state
const activeClass = this.config.classNames.captions.active;
// Get the next state
// If the method is called without parameter, toggle based on current value
const active = is.nullOrUndefined(input) ? !toggled : input;
// Update state and trigger event
if (active !== toggled) {
// When passive, don't override user preferences
if (!passive) {
this.captions.active = active;
this.storage.set({ captions: active });
}
// Force language if the call isn't passive and there is no matching language to toggle to
if (!this.language && active && !passive) {
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);
// Override user preferences to avoid switching languages if a matching track is added
this.captions.language = track.language;
// Set caption, but don't store in localStorage as user preference
captions.set.call(this, tracks.indexOf(track));
return;
}
// Toggle button if it's enabled
if (this.elements.buttons.captions) {
this.elements.buttons.captions.pressed = active;
}
// Add class hook
toggleClass(this.elements.container, activeClass, active);
this.captions.toggled = active;
// Update settings menu
controls.updateSetting.call(this, 'captions');
// Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
}
},
// Set captions by track index
// Used internally for the currentTrack setter with the passive option forced to false
set(index, passive = true) {
// Force language if the call isn't passive and there is no matching language to toggle to
if (!this.language && active && !passive) {
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);
// Disable captions if setting to -1
if (index === -1) {
captions.toggle.call(this, false, passive);
return;
}
// Override user preferences to avoid switching languages if a matching track is added
this.captions.language = track.language;
if (!is.number(index)) {
this.debug.warn('Invalid caption argument', index);
return;
}
// Set caption, but don't store in localStorage as user preference
captions.set.call(this, tracks.indexOf(track));
return;
}
if (!(index in tracks)) {
this.debug.warn('Track not found', index);
return;
}
// Toggle button if it's enabled
if (this.elements.buttons.captions) {
this.elements.buttons.captions.pressed = active;
}
if (this.captions.currentTrack !== index) {
this.captions.currentTrack = index;
const track = tracks[index];
const { language } = track || {};
// Add class hook
toggleClass(this.elements.container, activeClass, active);
// Store reference to node for invalidation on remove
this.captions.currentTrackNode = track;
this.captions.toggled = active;
// Update settings menu
controls.updateSetting.call(this, 'captions');
// Update settings menu
controls.updateSetting.call(this, 'captions');
// When passive, don't override user preferences
if (!passive) {
this.captions.language = language;
this.storage.set({ language });
}
// Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
}
// Handle Vimeo captions
if (this.isVimeo) {
this.embed.enableTextTrack(language);
}
// Wait for the call stack to clear before setting mode='hidden'
// on the active track - forcing the browser to download it
setTimeout(() => {
if (active && this.captions.toggled) {
this.captions.currentTrackNode.mode = 'hidden';
}
});
},
// Trigger event
triggerEvent.call(this, this.media, 'languagechange');
}
// Set captions by track index
// Used internally for the currentTrack setter with the passive option forced to false
set(index, passive = true) {
const tracks = captions.getTracks.call(this);
// Show captions
captions.toggle.call(this, true, passive);
// Disable captions if setting to -1
if (index === -1) {
captions.toggle.call(this, false, passive);
return;
}
if (this.isHTML5 && this.isVideo) {
// If we change the active track while a cue is already displayed we need to update it
captions.updateCues.call(this);
}
},
if (!is.number(index)) {
this.debug.warn('Invalid caption argument', index);
return;
}
// Set captions by language
// Used internally for the language setter with the passive option forced to false
setLanguage(input, passive = true) {
if (!is.string(input)) {
this.debug.warn('Invalid language argument', input);
return;
}
// Normalize
const language = input.toLowerCase();
if (!(index in tracks)) {
this.debug.warn('Track not found', index);
return;
}
if (this.captions.currentTrack !== index) {
this.captions.currentTrack = index;
const track = tracks[index];
const { language } = track || {};
// Store reference to node for invalidation on remove
this.captions.currentTrackNode = track;
// Update settings menu
controls.updateSetting.call(this, 'captions');
// When passive, don't override user preferences
if (!passive) {
this.captions.language = language;
this.storage.set({ language });
}
// Set currentTrack
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [language]);
captions.set.call(this, tracks.indexOf(track), passive);
},
// Handle Vimeo captions
if (this.isVimeo) {
this.embed.enableTextTrack(language);
}
// Get current valid caption tracks
// If update is false it will also ignore tracks without metadata
// This is used to "freeze" the language options when captions.update is false
getTracks(update = false) {
// Handle media or textTracks missing or null
const tracks = Array.from((this.media || {}).textTracks || []);
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
.filter(track => ['captions', 'subtitles'].includes(track.kind));
},
// Trigger event
triggerEvent.call(this, this.media, 'languagechange');
}
// Match tracks based on languages and get the first
findTrack(languages, force = false) {
const tracks = captions.getTracks.call(this);
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
let track;
languages.every(language => {
track = sorted.find(track => track.language === language);
return !track; // Break iteration if there is a match
});
// If no match is found but is required, get first
return track || (force ? sorted[0] : undefined);
},
// Show captions
captions.toggle.call(this, true, passive);
// Get the current track
getCurrentTrack() {
return captions.getTracks.call(this)[this.currentTrack];
},
if (this.isHTML5 && this.isVideo) {
// If we change the active track while a cue is already displayed we need to update it
captions.updateCues.call(this);
}
},
// Get UI label for track
getLabel(track) {
let currentTrack = track;
// Set captions by language
// Used internally for the language setter with the passive option forced to false
setLanguage(input, passive = true) {
if (!is.string(input)) {
this.debug.warn('Invalid language argument', input);
return;
}
// Normalize
const language = input.toLowerCase();
this.captions.language = language;
if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
currentTrack = captions.getCurrentTrack.call(this);
}
// Set currentTrack
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [language]);
captions.set.call(this, tracks.indexOf(track), passive);
},
if (is.track(currentTrack)) {
if (!is.empty(currentTrack.label)) {
return currentTrack.label;
}
// Get current valid caption tracks
// If update is false it will also ignore tracks without metadata
// This is used to "freeze" the language options when captions.update is false
getTracks(update = false) {
// Handle media or textTracks missing or null
const tracks = Array.from((this.media || {}).textTracks || []);
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks
.filter((track) => !this.isHTML5 || update || this.captions.meta.has(track))
.filter((track) => ['captions', 'subtitles'].includes(track.kind));
},
if (!is.empty(currentTrack.language)) {
return track.language.toUpperCase();
}
// Match tracks based on languages and get the first
findTrack(languages, force = false) {
const tracks = captions.getTracks.call(this);
const sortIsDefault = (track) => Number((this.captions.meta.get(track) || {}).default);
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
let track;
return i18n.get('enabled', this.config);
}
languages.every((language) => {
track = sorted.find((t) => t.language === language);
return !track; // Break iteration if there is a match
});
return i18n.get('disabled', this.config);
},
// If no match is found but is required, get first
return track || (force ? sorted[0] : undefined);
},
// Update captions using current track's active cues
// Also optional array argument in case there isn't any track (ex: vimeo)
updateCues(input) {
// Requires UI
if (!this.supported.ui) {
return;
}
// Get the current track
getCurrentTrack() {
return captions.getTracks.call(this)[this.currentTrack];
},
if (!is.element(this.elements.captions)) {
this.debug.warn('No captions element to render to');
return;
}
// Get UI label for track
getLabel(track) {
let currentTrack = track;
// Only accept array or empty input
if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
this.debug.warn('updateCues: Invalid input', input);
return;
}
if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
currentTrack = captions.getCurrentTrack.call(this);
}
let cues = input;
if (is.track(currentTrack)) {
if (!is.empty(currentTrack.label)) {
return currentTrack.label;
}
// Get cues from track
if (!cues) {
const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || [])
.map(cue => cue.getCueAsHTML())
.map(getHTML);
}
if (!is.empty(currentTrack.language)) {
return track.language.toUpperCase();
}
// Set new caption text
const content = cues.map(cueText => cueText.trim()).join('\n');
const changed = content !== this.elements.captions.innerHTML;
return i18n.get('enabled', this.config);
}
if (changed) {
// Empty the container and create a new child element
emptyElement(this.elements.captions);
const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
caption.innerHTML = content;
this.elements.captions.appendChild(caption);
return i18n.get('disabled', this.config);
},
// Trigger event
triggerEvent.call(this, this.media, 'cuechange');
}
},
// Update captions using current track's active cues
// Also optional array argument in case there isn't any track (ex: vimeo)
updateCues(input) {
// Requires UI
if (!this.supported.ui) {
return;
}
if (!is.element(this.elements.captions)) {
this.debug.warn('No captions element to render to');
return;
}
// Only accept array or empty input
if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
this.debug.warn('updateCues: Invalid input', input);
return;
}
let cues = input;
// Get cues from track
if (!cues) {
const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || [])
.map((cue) => cue.getCueAsHTML())
.map(getHTML);
}
// Set new caption text
const content = cues.map((cueText) => cueText.trim()).join('\n');
const changed = content !== this.elements.captions.innerHTML;
if (changed) {
// Empty the container and create a new child element
emptyElement(this.elements.captions);
const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
caption.innerHTML = content;
this.elements.captions.appendChild(caption);
// Trigger event
triggerEvent.call(this, this.media, 'cuechange');
}
},
};
export default captions;

View File

@ -3,398 +3,443 @@
// ==========================================================================
const defaults = {
// Disable
// Disable
enabled: true,
// Custom media title
title: '',
// Logging to console
debug: false,
// Auto play (if supported)
autoplay: false,
// Only allow one media playing at once (vimeo only)
autopause: true,
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
playsinline: true,
// Default time to skip when rewind/fast forward
seekTime: 10,
// Default volume
volume: 1,
muted: false,
// Pass a custom duration
duration: null,
// Display the media duration on load in the current time position
// If you have opted to display both duration and currentTime, this is ignored
displayDuration: true,
// Invert the current time to be a countdown
invertTime: true,
// Clicking the currentTime inverts it's value to show time left rather than elapsed
toggleInvert: true,
// Force an aspect ratio
// The format must be `'w:h'` (e.g. `'16:9'`)
ratio: null,
// Click video container to play/pause
clickToPlay: true,
// Auto hide the controls
hideControls: true,
// Reset to start when playback ended
resetOnEnd: false,
// Disable the standard context menu
disableContextMenu: true,
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.6.7/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
// Quality default
quality: {
default: 576,
// The options to display in the UI, if available for the source media
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
forced: false,
onChange: null,
},
// Set loops
loop: {
active: false,
// start: null,
// end: null,
},
// Speed default and options to display
speed: {
selected: 1,
// The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],
},
// Keyboard shortcut settings
keyboard: {
focused: true,
global: false,
},
// Display tooltips
tooltips: {
controls: false,
seek: true,
},
// Captions settings
captions: {
active: false,
language: 'auto',
// Listen to new tracks added after Plyr is initialized.
// This is needed for streaming captions, but may result in unselectable options
update: false,
},
// Fullscreen settings
fullscreen: {
enabled: true, // Allow fullscreen?
fallback: true, // Fallback using full viewport/window
iosNative: false, // Use the native fullscreen in iOS (disables custom controls)
// Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode
// Non-ancestors of the player element will be ignored
// container: null, // defaults to the player element
},
// Local storage
storage: {
enabled: true,
key: 'plyr',
},
// Custom media title
title: '',
// Default controls
controls: [
'play-large',
// 'restart',
// 'rewind',
'play',
// 'fast-forward',
'progress',
'current-time',
// 'duration',
'mute',
'volume',
'captions',
'settings',
'pip',
'airplay',
// 'download',
'fullscreen',
],
settings: ['captions', 'quality', 'speed'],
// Logging to console
debug: false,
// Auto play (if supported)
autoplay: false,
// Only allow one media playing at once (vimeo only)
autopause: true,
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
playsinline: true,
// Default time to skip when rewind/fast forward
seekTime: 10,
// Default volume
volume: 1,
muted: false,
// Pass a custom duration
duration: null,
// Display the media duration on load in the current time position
// If you have opted to display both duration and currentTime, this is ignored
displayDuration: true,
// Invert the current time to be a countdown
invertTime: true,
// Clicking the currentTime inverts it's value to show time left rather than elapsed
toggleInvert: true,
// Aspect ratio (for embeds)
ratio: '16:9',
// Click video container to play/pause
clickToPlay: true,
// Auto hide the controls
hideControls: true,
// Reset to start when playback ended
resetOnEnd: false,
// Disable the standard context menu
disableContextMenu: true,
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.3.12/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
// Quality default
quality: {
default: 576,
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
// Localisation
i18n: {
restart: 'Restart',
rewind: 'Rewind {seektime}s',
play: 'Play',
pause: 'Pause',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
duration: 'Duration',
volume: 'Volume',
mute: 'Mute',
unmute: 'Unmute',
enableCaptions: 'Enable captions',
disableCaptions: 'Disable captions',
download: 'Download',
enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen',
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
pip: 'PIP',
menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
loop: 'Loop',
start: 'Start',
end: 'End',
all: 'All',
reset: 'Reset',
disabled: 'Disabled',
enabled: 'Enabled',
advertisement: 'Ad',
qualityBadge: {
2160: '4K',
1440: 'HD',
1080: 'HD',
720: 'HD',
576: 'SD',
480: 'SD',
},
},
// Set loops
loop: {
active: false,
// start: null,
// end: null,
// URLs
urls: {
download: null,
vimeo: {
sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}',
api: 'https://vimeo.com/api/oembed.json?url={0}',
},
// Speed default and options to display
speed: {
selected: 1,
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
youtube: {
sdk: 'https://www.youtube.com/iframe_api',
api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',
},
// Keyboard shortcut settings
keyboard: {
focused: true,
global: false,
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
},
},
// Display tooltips
tooltips: {
controls: false,
seek: true,
// Custom control listeners
listeners: {
seek: null,
play: null,
pause: null,
restart: null,
rewind: null,
fastForward: null,
mute: null,
volume: null,
captions: null,
download: null,
fullscreen: null,
pip: null,
airplay: null,
speed: null,
quality: null,
loop: null,
language: null,
},
// Events to watch and bubble
events: [
// Events to watch on HTML5 media elements and bubble
// https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
'ended',
'progress',
'stalled',
'playing',
'waiting',
'canplay',
'canplaythrough',
'loadstart',
'loadeddata',
'loadedmetadata',
'timeupdate',
'volumechange',
'play',
'pause',
'error',
'seeking',
'seeked',
'emptied',
'ratechange',
'cuechange',
// Custom events
'download',
'enterfullscreen',
'exitfullscreen',
'captionsenabled',
'captionsdisabled',
'languagechange',
'controlshidden',
'controlsshown',
'ready',
// YouTube
'statechange',
// Quality
'qualitychange',
// Ads
'adsloaded',
'adscontentpause',
'adscontentresume',
'adstarted',
'adsmidpoint',
'adscomplete',
'adsallcomplete',
'adsimpression',
'adsclick',
],
// Selectors
// Change these to match your template if using custom HTML
selectors: {
editable: 'input, textarea, select, [contenteditable]',
container: '.plyr',
controls: {
container: null,
wrapper: '.plyr__controls',
},
labels: '[data-plyr]',
buttons: {
play: '[data-plyr="play"]',
pause: '[data-plyr="pause"]',
restart: '[data-plyr="restart"]',
rewind: '[data-plyr="rewind"]',
fastForward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]',
download: '[data-plyr="download"]',
fullscreen: '[data-plyr="fullscreen"]',
pip: '[data-plyr="pip"]',
airplay: '[data-plyr="airplay"]',
settings: '[data-plyr="settings"]',
loop: '[data-plyr="loop"]',
},
inputs: {
seek: '[data-plyr="seek"]',
volume: '[data-plyr="volume"]',
speed: '[data-plyr="speed"]',
language: '[data-plyr="language"]',
quality: '[data-plyr="quality"]',
},
display: {
currentTime: '.plyr__time--current',
duration: '.plyr__time--duration',
buffer: '.plyr__progress__buffer',
loop: '.plyr__progress__loop', // Used later
volume: '.plyr__volume--display',
},
progress: '.plyr__progress',
captions: '.plyr__captions',
caption: '.plyr__caption',
},
// Captions settings
// Class hooks added to the player in different states
classNames: {
type: 'plyr--{0}',
provider: 'plyr--{0}',
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',
embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster',
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading',
hover: 'plyr--hover',
tooltip: 'plyr__tooltip',
cues: 'plyr__cues',
hidden: 'plyr__sr-only',
hideControls: 'plyr--hide-controls',
isIos: 'plyr--is-ios',
isTouch: 'plyr--is-touch',
uiSupported: 'plyr--full-ui',
noTransition: 'plyr--no-transition',
display: {
time: 'plyr__time',
},
menu: {
value: 'plyr__menu__value',
badge: 'plyr__badge',
open: 'plyr--menu-open',
},
captions: {
active: false,
language: 'auto',
// Listen to new tracks added after Plyr is initialized.
// This is needed for streaming captions, but may result in unselectable options
update: false,
enabled: 'plyr--captions-enabled',
active: 'plyr--captions-active',
},
// Fullscreen settings
fullscreen: {
enabled: true, // Allow fullscreen?
fallback: true, // Fallback for vintage browsers
iosNative: false, // Use the native fullscreen in iOS (disables custom controls)
enabled: 'plyr--fullscreen-enabled',
fallback: 'plyr--fullscreen-fallback',
},
// Local storage
storage: {
enabled: true,
key: 'plyr',
pip: {
supported: 'plyr--pip-supported',
active: 'plyr--pip-active',
},
// Default controls
controls: [
'play-large',
// 'restart',
// 'rewind',
'play',
// 'fast-forward',
'progress',
'current-time',
'mute',
'volume',
'captions',
'settings',
'pip',
'airplay',
// 'download',
'fullscreen',
],
settings: ['captions', 'quality', 'speed'],
// Localisation
i18n: {
restart: 'Restart',
rewind: 'Rewind {seektime}s',
play: 'Play',
pause: 'Pause',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
duration: 'Duration',
volume: 'Volume',
mute: 'Mute',
unmute: 'Unmute',
enableCaptions: 'Enable captions',
disableCaptions: 'Disable captions',
download: 'Download',
enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen',
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
loop: 'Loop',
start: 'Start',
end: 'End',
all: 'All',
reset: 'Reset',
disabled: 'Disabled',
enabled: 'Enabled',
advertisement: 'Ad',
qualityBadge: {
2160: '4K',
1440: 'HD',
1080: 'HD',
720: 'HD',
576: 'SD',
480: 'SD',
},
airplay: {
supported: 'plyr--airplay-supported',
active: 'plyr--airplay-active',
},
// URLs
urls: {
download: null,
vimeo: {
sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}',
api: 'https://vimeo.com/api/v2/video/{0}.json',
},
youtube: {
sdk: 'https://www.youtube.com/iframe_api',
api:
'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
},
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
},
tabFocus: 'plyr__tab-focus',
previewThumbnails: {
// Tooltip thumbs
thumbContainer: 'plyr__preview-thumb',
thumbContainerShown: 'plyr__preview-thumb--is-shown',
imageContainer: 'plyr__preview-thumb__image-container',
timeContainer: 'plyr__preview-thumb__time-container',
// Scrubbing
scrubbingContainer: 'plyr__preview-scrubbing',
scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',
},
},
// Custom control listeners
listeners: {
seek: null,
play: null,
pause: null,
restart: null,
rewind: null,
fastForward: null,
mute: null,
volume: null,
captions: null,
download: null,
fullscreen: null,
pip: null,
airplay: null,
speed: null,
quality: null,
loop: null,
language: null,
// Embed attributes
attributes: {
embed: {
provider: 'data-plyr-provider',
id: 'data-plyr-embed-id',
},
},
// Events to watch and bubble
events: [
// Events to watch on HTML5 media elements and bubble
// https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
'ended',
'progress',
'stalled',
'playing',
'waiting',
'canplay',
'canplaythrough',
'loadstart',
'loadeddata',
'loadedmetadata',
'timeupdate',
'volumechange',
'play',
'pause',
'error',
'seeking',
'seeked',
'emptied',
'ratechange',
'cuechange',
// Advertisements plugin
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: {
enabled: false,
publisherId: '',
tagUrl: '',
},
// Custom events
'download',
'enterfullscreen',
'exitfullscreen',
'captionsenabled',
'captionsdisabled',
'languagechange',
'controlshidden',
'controlsshown',
'ready',
// Preview Thumbnails plugin
previewThumbnails: {
enabled: false,
src: '',
},
// YouTube
'statechange',
// Vimeo plugin
vimeo: {
byline: false,
portrait: false,
title: false,
speed: true,
transparent: false,
// Custom settings from Plyr
customControls: true,
referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
// Whether the owner of the video has a Pro or Business account
// (which allows us to properly hide controls without CSS hacks, etc)
premium: false,
},
// Quality
'qualitychange',
// Ads
'adsloaded',
'adscontentpause',
'adscontentresume',
'adstarted',
'adsmidpoint',
'adscomplete',
'adsallcomplete',
'adsimpression',
'adsclick',
],
// Selectors
// Change these to match your template if using custom HTML
selectors: {
editable: 'input, textarea, select, [contenteditable]',
container: '.plyr',
controls: {
container: null,
wrapper: '.plyr__controls',
},
labels: '[data-plyr]',
buttons: {
play: '[data-plyr="play"]',
pause: '[data-plyr="pause"]',
restart: '[data-plyr="restart"]',
rewind: '[data-plyr="rewind"]',
fastForward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]',
download: '[data-plyr="download"]',
fullscreen: '[data-plyr="fullscreen"]',
pip: '[data-plyr="pip"]',
airplay: '[data-plyr="airplay"]',
settings: '[data-plyr="settings"]',
loop: '[data-plyr="loop"]',
},
inputs: {
seek: '[data-plyr="seek"]',
volume: '[data-plyr="volume"]',
speed: '[data-plyr="speed"]',
language: '[data-plyr="language"]',
quality: '[data-plyr="quality"]',
},
display: {
currentTime: '.plyr__time--current',
duration: '.plyr__time--duration',
buffer: '.plyr__progress__buffer',
loop: '.plyr__progress__loop', // Used later
volume: '.plyr__volume--display',
},
progress: '.plyr__progress',
captions: '.plyr__captions',
caption: '.plyr__caption',
menu: {
quality: '.js-plyr__menu__list--quality',
},
},
// Class hooks added to the player in different states
classNames: {
type: 'plyr--{0}',
provider: 'plyr--{0}',
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster',
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading',
hover: 'plyr--hover',
tooltip: 'plyr__tooltip',
cues: 'plyr__cues',
hidden: 'plyr__sr-only',
hideControls: 'plyr--hide-controls',
isIos: 'plyr--is-ios',
isTouch: 'plyr--is-touch',
uiSupported: 'plyr--full-ui',
noTransition: 'plyr--no-transition',
display: {
time: 'plyr__time',
},
menu: {
value: 'plyr__menu__value',
badge: 'plyr__badge',
open: 'plyr--menu-open',
},
captions: {
enabled: 'plyr--captions-enabled',
active: 'plyr--captions-active',
},
fullscreen: {
enabled: 'plyr--fullscreen-enabled',
fallback: 'plyr--fullscreen-fallback',
},
pip: {
supported: 'plyr--pip-supported',
active: 'plyr--pip-active',
},
airplay: {
supported: 'plyr--airplay-supported',
active: 'plyr--airplay-active',
},
tabFocus: 'plyr__tab-focus',
},
// Embed attributes
attributes: {
embed: {
provider: 'data-plyr-provider',
id: 'data-plyr-embed-id',
},
},
// API keys
keys: {
google: null,
},
// Advertisements plugin
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: {
enabled: false,
publisherId: '',
},
// YouTube plugin
youtube: {
rel: 0, // No related vids
showinfo: 0, // Hide info
iv_load_policy: 3, // Hide annotations
modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)
// Custom settings from Plyr
customControls: true,
noCookie: false, // Whether to use an alternative version of YouTube without cookies
},
};
export default defaults;

10
src/js/config/states.js Normal file
View File

@ -0,0 +1,10 @@
// ==========================================================================
// Plyr states
// ==========================================================================
export const pip = {
active: 'picture-in-picture',
inactive: 'inline',
};
export default { pip };

View File

@ -3,14 +3,14 @@
// ==========================================================================
export const providers = {
html5: 'html5',
youtube: 'youtube',
vimeo: 'vimeo',
html5: 'html5',
youtube: 'youtube',
vimeo: 'vimeo',
};
export const types = {
audio: 'audio',
video: 'video',
audio: 'audio',
video: 'video',
};
/**
@ -18,17 +18,17 @@ export const types = {
* @param {String} url
*/
export function getProviderByUrl(url) {
// YouTube
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) {
return providers.youtube;
}
// YouTube
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) {
return providers.youtube;
}
// Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo;
}
// Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo;
}
return null;
return null;
}
export default { providers, types };

View File

@ -5,26 +5,26 @@
const noop = () => {};
export default class Console {
constructor(enabled = false) {
this.enabled = window.console && enabled;
constructor(enabled = false) {
this.enabled = window.console && enabled;
if (this.enabled) {
this.log('Debugging enabled');
}
if (this.enabled) {
this.log('Debugging enabled');
}
}
get log() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
}
get log() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
}
get warn() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
}
get warn() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
}
get error() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
}
get error() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
}
}

3148
src/js/controls.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -4,41 +4,164 @@
// https://webkit.org/blog/7929/designing-websites-for-iphone-x/
// ==========================================================================
import { repaint } from './utils/animation';
import browser from './utils/browser';
import { hasClass, toggleClass, trapFocus } from './utils/elements';
import { closest, getElements, hasClass, toggleClass } from './utils/elements';
import { on, triggerEvent } from './utils/events';
import is from './utils/is';
import { silencePromise } from './utils/promise';
function onChange() {
if (!this.enabled) {
class Fullscreen {
constructor(player) {
// Keep reference to parent
this.player = player;
// Get prefix
this.prefix = Fullscreen.prefix;
this.property = Fullscreen.property;
// Scroll position
this.scrollPosition = { x: 0, y: 0 };
// Force the use of 'full window/browser' rather than fullscreen
this.forceFallback = player.config.fullscreen.fallback === 'force';
// Get the fullscreen element
// Checks container is an ancestor, defaults to null
this.player.elements.fullscreen =
player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container);
// Register event listeners
// Handle event (incase user presses escape etc)
on.call(
this.player,
document,
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => {
// TODO: Filter for target??
this.onChange();
},
);
// Fullscreen toggle on double click
on.call(this.player, this.player.elements.container, 'dblclick', (event) => {
// Ignore double click in controls
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return;
}
this.player.listeners.proxy(event, this.toggle, 'fullscreen');
});
// Tap focus when in fullscreen
on.call(this, this.player.elements.container, 'keydown', (event) => this.trapFocus(event));
// Update the UI
this.update();
// this.toggle = this.toggle.bind(this);
}
// Determine if native supported
static get native() {
return !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled
);
}
// If we're actually using native
get usingNative() {
return Fullscreen.native && !this.forceFallback;
}
// Get the prefix for handlers
static get prefix() {
// No prefix
if (is.function(document.exitFullscreen)) {
return '';
}
// Check for fullscreen support by vendor prefix
let value = '';
const prefixes = ['webkit', 'moz', 'ms'];
prefixes.some((pre) => {
if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
value = pre;
return true;
}
return false;
});
return value;
}
static get property() {
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
}
// Determine if fullscreen is enabled
get enabled() {
return (
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
this.player.config.fullscreen.enabled &&
this.player.supported.ui &&
this.player.isVideo
);
}
// Get active state
get active() {
if (!this.enabled) {
return false;
}
// Fallback using classname
if (!Fullscreen.native || this.forceFallback) {
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
}
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;
}
// Get target element
get target() {
return browser.isIos && this.player.config.fullscreen.iosNative
? this.player.media
: this.player.elements.fullscreen || this.player.elements.container;
}
onChange = () => {
if (!this.enabled) {
return;
}
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (is.element(button)) {
button.pressed = this.active;
button.pressed = this.active;
}
// Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up
const target = this.target === this.player.media ? this.target : this.player.elements.container;
// Trigger an event
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
};
// Trap focus in container
if (!browser.isIos) {
trapFocus.call(this.player, this.target, this.active);
}
}
function toggleFallback(toggle = false) {
toggleFallback = (toggle = false) => {
// Store or restore scroll position
if (toggle) {
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
}
// Toggle scroll
@ -49,205 +172,132 @@ function toggleFallback(toggle = false) {
// Force full viewport on iPhone X+
if (browser.isIos) {
let viewport = document.head.querySelector('meta[name="viewport"]');
const property = 'viewport-fit=cover';
let viewport = document.head.querySelector('meta[name="viewport"]');
const property = 'viewport-fit=cover';
// Inject the viewport meta if required
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
// Inject the viewport meta if required
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
}
// Check if the property already exists
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) {
viewport.content += `,${property}`;
}
// Check if the property already exists
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) {
viewport.content += `,${property}`;
}
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
.filter(part => part.trim() !== property)
.join(',');
}
// Force a repaint as sometimes Safari doesn't want to fill the screen
setTimeout(() => repaint(this.target), 100);
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
.filter((part) => part.trim() !== property)
.join(',');
}
}
// Toggle button and fire events
onChange.call(this);
}
this.onChange();
};
class Fullscreen {
constructor(player) {
// Keep reference to parent
this.player = player;
// Get prefix
this.prefix = Fullscreen.prefix;
this.property = Fullscreen.property;
// Scroll position
this.scrollPosition = { x: 0, y: 0 };
// Register event listeners
// Handle event (incase user presses escape etc)
on.call(
this.player,
document,
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => {
// TODO: Filter for target??
onChange.call(this);
},
);
// Fullscreen toggle on double click
on.call(this.player, this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return;
}
this.toggle();
});
// Update the UI
this.update();
// Trap focus inside container
trapFocus = (event) => {
// Bail if iOS, not active, not the tab key
if (browser.isIos || !this.active || event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Determine if native supported
static get native() {
return !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled
);
// Get the current focused element
const focused = document.activeElement;
const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');
const [first] = focusable;
const last = focusable[focusable.length - 1];
if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used
first.focus();
event.preventDefault();
} else if (focused === first && event.shiftKey) {
// Move focus to last element that can be tabbed if Shift is used
last.focus();
event.preventDefault();
}
};
// Update UI
update = () => {
if (this.enabled) {
let mode;
if (this.forceFallback) {
mode = 'Fallback (forced)';
} else if (Fullscreen.native) {
mode = 'Native';
} else {
mode = 'Fallback';
}
this.player.debug.log(`${mode} fullscreen enabled`);
} else {
this.player.debug.log('Fullscreen not supported and fallback disabled');
}
// Get the prefix for handlers
static get prefix() {
// No prefix
if (is.function(document.exitFullscreen)) {
return '';
}
// Add styling hook to show button
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
};
// Check for fullscreen support by vendor prefix
let value = '';
const prefixes = ['webkit', 'moz', 'ms'];
prefixes.some(pre => {
if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
value = pre;
return true;
}
return false;
});
return value;
// Make an element fullscreen
enter = () => {
if (!this.enabled) {
return;
}
static get property() {
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
// iOS native fullscreen doesn't need the request step
if (browser.isIos && this.player.config.fullscreen.iosNative) {
if (this.player.isVimeo) {
this.player.embed.requestFullscreen();
} else {
this.target.webkitEnterFullscreen();
}
} else if (!Fullscreen.native || this.forceFallback) {
this.toggleFallback(true);
} else if (!this.prefix) {
this.target.requestFullscreen({ navigationUI: 'hide' });
} else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`]();
}
};
// Bail from fullscreen
exit = () => {
if (!this.enabled) {
return;
}
// Determine if fullscreen is enabled
get enabled() {
return (
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
this.player.config.fullscreen.enabled &&
this.player.supported.ui &&
this.player.isVideo
);
// iOS native fullscreen
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen();
silencePromise(this.player.play());
} else if (!Fullscreen.native || this.forceFallback) {
this.toggleFallback(false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.property}`]();
}
};
// Get active state
get active() {
if (!this.enabled) {
return false;
}
// Fallback using classname
if (!Fullscreen.native) {
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
}
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
return element === this.target;
}
// Get target element
get target() {
return browser.isIos && this.player.config.fullscreen.iosNative
? this.player.media
: this.player.elements.container;
}
// Update UI
update() {
if (this.enabled) {
this.player.debug.log(`${Fullscreen.native ? 'Native' : 'Fallback'} fullscreen enabled`);
} else {
this.player.debug.log('Fullscreen not supported and fallback disabled');
}
// Add styling hook to show button
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
}
// Make an element fullscreen
enter() {
if (!this.enabled) {
return;
}
// iOS native fullscreen doesn't need the request step
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitEnterFullscreen();
} else if (!Fullscreen.native) {
toggleFallback.call(this, true);
} else if (!this.prefix) {
this.target.requestFullscreen();
} else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`]();
}
}
// Bail from fullscreen
exit() {
if (!this.enabled) {
return;
}
// iOS native fullscreen
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen();
this.player.play();
} else if (!Fullscreen.native) {
toggleFallback.call(this, false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.property}`]();
}
}
// Toggle state
toggle() {
if (!this.active) {
this.enter();
} else {
this.exit();
}
// Toggle state
toggle = () => {
if (!this.active) {
this.enter();
} else {
this.exit();
}
};
}
export default Fullscreen;

View File

@ -5,113 +5,143 @@
import support from './support';
import { removeElement } from './utils/elements';
import { triggerEvent } from './utils/events';
import is from './utils/is';
import { silencePromise } from './utils/promise';
import { setAspectRatio } from './utils/style';
const html5 = {
getSources() {
if (!this.isHTML5) {
return [];
getSources() {
if (!this.isHTML5) {
return [];
}
const sources = Array.from(this.media.querySelectorAll('source'));
// Filter out unsupported sources (if type is specified)
return sources.filter((source) => {
const type = source.getAttribute('type');
if (is.empty(type)) {
return true;
}
return support.mime.call(this, type);
});
},
// Get quality levels
getQualityOptions() {
// Whether we're forcing all options (e.g. for streaming)
if (this.config.quality.forced) {
return this.config.quality.options;
}
// Get sizes from <source> elements
return html5.getSources
.call(this)
.map((source) => Number(source.getAttribute('size')))
.filter(Boolean);
},
setup() {
if (!this.isHTML5) {
return;
}
const player = this;
// Set speed options from config
player.options.speed = player.config.speed.options;
// Set aspect ratio if fixed
if (!is.empty(this.config.ratio)) {
setAspectRatio.call(player);
}
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
// Get sources
const sources = html5.getSources.call(player);
const source = sources.find((s) => s.getAttribute('src') === player.source);
// Return size, if match is found
return source && Number(source.getAttribute('size'));
},
set(input) {
if (player.quality === input) {
return;
}
const sources = Array.from(this.media.querySelectorAll('source'));
// If we're using an an external handler...
if (player.config.quality.forced && is.function(player.config.quality.onChange)) {
player.config.quality.onChange(input);
} else {
// Get sources
const sources = html5.getSources.call(player);
// Get first match for requested size
const source = sources.find((s) => Number(s.getAttribute('size')) === input);
// Filter out unsupported sources
return sources.filter(source => support.mime.call(this, source.getAttribute('type')));
},
// Get quality levels
getQualityOptions() {
// Get sizes from <source> elements
return html5.getSources
.call(this)
.map(source => Number(source.getAttribute('size')))
.filter(Boolean);
},
extend() {
if (!this.isHTML5) {
// No matching source found
if (!source) {
return;
}
// Get current state
const { currentTime, paused, preload, readyState, playbackRate } = player.media;
// Set new source
player.media.src = source.getAttribute('src');
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
if (preload !== 'none' || readyState) {
// Restore time
player.once('loadedmetadata', () => {
player.speed = playbackRate;
player.currentTime = currentTime;
// Resume playing
if (!paused) {
silencePromise(player.play());
}
});
// Load new source
player.media.load();
}
}
const player = this;
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
// Get sources
const sources = html5.getSources.call(player);
const source = sources.find(source => source.getAttribute('src') === player.source);
// Return size, if match is found
return source && Number(source.getAttribute('size'));
},
set(input) {
// Get sources
const sources = html5.getSources.call(player);
// Get first match for requested size
const source = sources.find(source => Number(source.getAttribute('size')) === input);
// No matching source found
if (!source) {
return;
}
// Get current state
const { currentTime, paused, preload, readyState } = player.media;
// Set new source
player.media.src = source.getAttribute('src');
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
if (preload !== 'none' || readyState) {
// Restore time
player.once('loadedmetadata', () => {
player.currentTime = currentTime;
// Resume playing
if (!paused) {
player.play();
}
});
// Load new source
player.media.load();
}
// Trigger change event
triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: input,
});
// Save to storage
player.storage.set({ quality: input });
},
// Trigger change event
triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: input,
});
},
},
});
},
// Cancel current network requests
// Cancel current network requests
// See https://github.com/sampotts/plyr/issues/174
cancelRequests() {
if (!this.isHTML5) {
return;
}
// Remove child sources
removeElement(html5.getSources.call(this));
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
this.media.setAttribute('src', this.config.blankVideo);
// Load the new empty source
// This will cancel existing requests
// See https://github.com/sampotts/plyr/issues/174
cancelRequests() {
if (!this.isHTML5) {
return;
}
this.media.load();
// Remove child sources
removeElement(html5.getSources.call(this));
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
this.media.setAttribute('src', this.config.blankVideo);
// Load the new empty source
// This will cancel existing requests
// See https://github.com/sampotts/plyr/issues/174
this.media.load();
// Debugging
this.debug.log('Cancelled network requests');
},
// Debugging
this.debug.log('Cancelled network requests');
},
};
export default html5;

File diff suppressed because it is too large Load Diff

View File

@ -8,52 +8,52 @@ import youtube from './plugins/youtube';
import { createElement, toggleClass, wrap } from './utils/elements';
const media = {
// Setup media
setup() {
// If there's no media, bail
if (!this.media) {
this.debug.warn('No media element found!');
return;
}
// Setup media
setup() {
// If there's no media, bail
if (!this.media) {
this.debug.warn('No media element found!');
return;
}
// Add type class
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
// Add type class
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
// Add provider class
toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
// Add provider class
toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
// Add video class for embeds
// This will require changes if audio embeds are added
if (this.isEmbed) {
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
}
// Add video class for embeds
// This will require changes if audio embeds are added
if (this.isEmbed) {
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
}
// Inject the player wrapper
if (this.isVideo) {
// Create the wrapper div
this.elements.wrapper = createElement('div', {
class: this.config.classNames.video,
});
// Inject the player wrapper
if (this.isVideo) {
// Create the wrapper div
this.elements.wrapper = createElement('div', {
class: this.config.classNames.video,
});
// Wrap the video in a container
wrap(this.media, this.elements.wrapper);
// Wrap the video in a container
wrap(this.media, this.elements.wrapper);
// Faux poster container
this.elements.poster = createElement('div', {
class: this.config.classNames.poster,
});
// Poster image container
this.elements.poster = createElement('div', {
class: this.config.classNames.poster,
});
this.elements.wrapper.appendChild(this.elements.poster);
}
this.elements.wrapper.appendChild(this.elements.poster);
}
if (this.isHTML5) {
html5.extend.call(this);
} else if (this.isYouTube) {
youtube.setup.call(this);
} else if (this.isVimeo) {
vimeo.setup.call(this);
}
},
if (this.isHTML5) {
html5.setup.call(this);
} else if (this.isYouTube) {
youtube.setup.call(this);
} else if (this.isVimeo) {
vimeo.setup.call(this);
}
},
};
export default media;

File diff suppressed because it is too large Load Diff

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;

View File

@ -9,404 +9,409 @@ import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import loadScript from '../utils/loadScript';
import loadScript from '../utils/load-script';
import { format, stripHTML } from '../utils/strings';
import { roundAspectRatio, setAspectRatio } from '../utils/style';
import { buildUrlParams } from '../utils/urls';
// Parse Vimeo ID from URL
function parseId(url) {
if (is.empty(url)) {
return null;
}
if (is.empty(url)) {
return null;
}
if (is.number(Number(url))) {
return url;
}
if (is.number(Number(url))) {
return url;
}
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
return url.match(regex) ? RegExp.$2 : url;
}
// Get aspect ratio for dimensions
function getAspectRatio(width, height) {
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
const ratio = getRatio(width, height);
return `${width / ratio}:${height / ratio}`;
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
return url.match(regex) ? RegExp.$2 : url;
}
// Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) {
this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) {
this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
const vimeo = {
setup() {
// Add embed class for responsive
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
setup() {
const player = this;
// Set intial ratio
vimeo.setAspectRatio.call(this);
// Add embed class for responsive
toggleClass(player.elements.wrapper, player.config.classNames.embed, true);
// Load the API if not already
if (!is.object(window.Vimeo)) {
loadScript(this.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(this);
})
.catch(error => {
this.debug.warn('Vimeo API failed to load', error);
});
} else {
vimeo.ready.call(this);
}
},
// Set speed options from config
player.options.speed = player.config.speed.options;
// Set aspect ratio
// For Vimeo we have an extra 300% height <div> to hide the standard controls and UI
setAspectRatio(input) {
const [x, y] = (is.string(input) ? input : this.config.ratio).split(':');
const padding = (100 / x) * y;
this.elements.wrapper.style.paddingBottom = `${padding}%`;
// Set intial ratio
setAspectRatio.call(player);
if (this.supported.ui) {
const height = 240;
const offset = (height - padding) / (height / 50);
// Load the SDK if not already
if (!is.object(window.Vimeo)) {
loadScript(player.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(player);
})
.catch((error) => {
player.debug.warn('Vimeo SDK (player.js) failed to load', error);
});
} else {
vimeo.ready.call(player);
}
},
this.media.style.transform = `translateY(-${offset}%)`;
}
},
// API Ready
ready() {
const player = this;
const config = player.config.vimeo;
const { premium, referrerPolicy, ...frameParams } = config;
// API Ready
ready() {
const player = this;
// If the owner has a pro or premium account then we can hide controls etc
if (premium) {
Object.assign(frameParams, {
controls: false,
sidedock: false,
});
}
// Get Vimeo params for the iframe
const options = {
loop: player.config.loop.active,
autoplay: player.autoplay,
// muted: player.muted,
byline: false,
portrait: false,
title: false,
speed: true,
transparent: 0,
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
};
const params = buildUrlParams(options);
// Get Vimeo params for the iframe
const params = buildUrlParams({
loop: player.config.loop.active,
autoplay: player.autoplay,
muted: player.muted,
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
...frameParams,
});
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(player.config.attributes.embed.id);
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(player.config.attributes.embed.id);
}
const id = parseId(source);
// Build an iframe
const iframe = createElement('iframe');
const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute(
'allow',
['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '),
);
// Set the referrer policy if required
if (!is.empty(referrerPolicy)) {
iframe.setAttribute('referrerPolicy', referrerPolicy);
}
// Inject the package
if (premium || !config.customControls) {
iframe.setAttribute('data-poster', player.poster);
player.media = replaceElement(iframe, player.media);
} else {
const wrapper = createElement('div', {
class: player.config.classNames.embedContainer,
'data-poster': player.poster,
});
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
}
// Get poster image
if (!config.customControls) {
fetch(format(player.config.urls.vimeo.api, src)).then((response) => {
if (is.empty(response) || !response.thumbnail_url) {
return;
}
const id = parseId(source);
// Set and show poster
ui.setPoster.call(player, response.thumbnail_url).catch(() => {});
});
}
// Build an iframe
const iframe = createElement('iframe');
const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay');
// Setup instance
// https://github.com/vimeo/player.js
player.embed = new window.Vimeo.Player(iframe, {
autopause: player.config.autopause,
muted: player.muted,
});
// Get poster, if already set
const { poster } = player;
player.media.paused = true;
player.media.currentTime = 0;
// Inject the package
const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
// Disable native text track rendering
if (player.supported.ui) {
player.embed.disableTextTrack();
}
// Get poster image
fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (is.empty(response)) {
return;
}
// Create a faux HTML5 API using the Vimeo API
player.media.play = () => {
assurePlaybackState.call(player, true);
return player.embed.play();
};
// Get the URL for thumbnail
const url = new URL(response[0].thumbnail_large);
player.media.pause = () => {
assurePlaybackState.call(player, false);
return player.embed.pause();
};
// Get original image
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
player.media.stop = () => {
player.pause();
player.currentTime = 0;
};
// Set and show poster
ui.setPoster.call(player, url.href).catch(() => {});
});
// Seeking
let { currentTime } = player.media;
Object.defineProperty(player.media, 'currentTime', {
get() {
return currentTime;
},
set(time) {
// Vimeo will automatically play on seek if the video hasn't been played before
// Setup instance
// https://github.com/vimeo/player.js
player.embed = new window.Vimeo.Player(iframe, {
autopause: player.config.autopause,
muted: player.muted,
});
// Get current paused state and volume etc
const { embed, media, paused, volume } = player;
const restorePause = paused && !embed.hasPlayed;
player.media.paused = true;
player.media.currentTime = 0;
// Set seeking state and trigger event
media.seeking = true;
triggerEvent.call(player, media, 'seeking');
// Disable native text track rendering
if (player.supported.ui) {
player.embed.disableTextTrack();
}
// If paused, mute until seek is complete
Promise.resolve(restorePause && embed.setVolume(0))
// Seek
.then(() => embed.setCurrentTime(time))
// Restore paused
.then(() => restorePause && embed.pause())
// Restore volume
.then(() => restorePause && embed.setVolume(volume))
.catch(() => {
// Do nothing
});
},
});
// Create a faux HTML5 API using the Vimeo API
player.media.play = () => {
assurePlaybackState.call(player, true);
return player.embed.play();
};
player.media.pause = () => {
assurePlaybackState.call(player, false);
return player.embed.pause();
};
player.media.stop = () => {
player.pause();
player.currentTime = 0;
};
// Seeking
let { currentTime } = player.media;
Object.defineProperty(player.media, 'currentTime', {
get() {
return currentTime;
},
set(time) {
// Vimeo will automatically play on seek if the video hasn't been played before
// Get current paused state and volume etc
const { embed, media, paused, volume } = player;
const restorePause = paused && !embed.hasPlayed;
// Set seeking state and trigger event
media.seeking = true;
triggerEvent.call(player, media, 'seeking');
// If paused, mute until seek is complete
Promise.resolve(restorePause && embed.setVolume(0))
// Seek
.then(() => embed.setCurrentTime(time))
// Restore paused
.then(() => restorePause && embed.pause())
// Restore volume
.then(() => restorePause && embed.setVolume(volume))
.catch(() => {
// Do nothing
});
},
});
// Playback speed
let speed = player.config.speed.selected;
Object.defineProperty(player.media, 'playbackRate', {
get() {
return speed;
},
set(input) {
player.embed
.setPlaybackRate(input)
.then(() => {
speed = input;
triggerEvent.call(player, player.media, 'ratechange');
})
.catch(error => {
// Hide menu item (and menu if empty)
if (error.name === 'Error') {
controls.setSpeedMenu.call(player, []);
}
});
},
});
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
get() {
return volume;
},
set(input) {
player.embed.setVolume(input).then(() => {
volume = input;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Muted
let { muted } = player.config;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : false;
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
muted = toggle;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Loop
let { loop } = player.config;
Object.defineProperty(player.media, 'loop', {
get() {
return loop;
},
set(input) {
const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => {
loop = toggle;
});
},
});
// Source
let currentSrc;
// Playback speed
let speed = player.config.speed.selected;
Object.defineProperty(player.media, 'playbackRate', {
get() {
return speed;
},
set(input) {
player.embed
.getVideoUrl()
.then(value => {
currentSrc = value;
controls.setDownloadLink.call(player);
})
.catch(error => {
this.debug.warn(error);
});
.setPlaybackRate(input)
.then(() => {
speed = input;
triggerEvent.call(player, player.media, 'ratechange');
})
.catch(() => {
// Cannot set Playback Rate, Video is probably not on Pro account
player.options.speed = [1];
});
},
});
Object.defineProperty(player.media, 'currentSrc', {
get() {
return currentSrc;
},
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
get() {
return volume;
},
set(input) {
player.embed.setVolume(input).then(() => {
volume = input;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Ended
Object.defineProperty(player.media, 'ended', {
get() {
return player.currentTime === player.duration;
},
// Muted
let { muted } = player.config;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : false;
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
muted = toggle;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Set aspect ratio based on video size
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
const ratio = getAspectRatio(dimensions[0], dimensions[1]);
vimeo.setAspectRatio.call(this, ratio);
// Loop
let { loop } = player.config;
Object.defineProperty(player.media, 'loop', {
get() {
return loop;
},
set(input) {
const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => {
loop = toggle;
});
},
});
// Set autopause
player.embed.setAutopause(player.config.autopause).then(state => {
player.config.autopause = state;
});
// Source
let currentSrc;
player.embed
.getVideoUrl()
.then((value) => {
currentSrc = value;
controls.setDownloadUrl.call(player);
})
.catch((error) => {
this.debug.warn(error);
});
// Get title
player.embed.getVideoTitle().then(title => {
player.config.title = title;
ui.setTitle.call(this);
});
Object.defineProperty(player.media, 'currentSrc', {
get() {
return currentSrc;
},
});
// Get current time
player.embed.getCurrentTime().then(value => {
currentTime = value;
triggerEvent.call(player, player.media, 'timeupdate');
});
// Ended
Object.defineProperty(player.media, 'ended', {
get() {
return player.currentTime === player.duration;
},
});
// Get duration
player.embed.getDuration().then(value => {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
});
// Set aspect ratio based on video size
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then((dimensions) => {
const [width, height] = dimensions;
player.embed.ratio = roundAspectRatio(width, height);
setAspectRatio.call(this);
});
// Get captions
player.embed.getTextTracks().then(tracks => {
player.media.textTracks = tracks;
captions.setup.call(player);
});
// Set autopause
player.embed.setAutopause(player.config.autopause).then((state) => {
player.config.autopause = state;
});
player.embed.on('cuechange', ({ cues = [] }) => {
const strippedCues = cues.map(cue => stripHTML(cue.text));
captions.updateCues.call(player, strippedCues);
});
// Get title
player.embed.getVideoTitle().then((title) => {
player.config.title = title;
ui.setTitle.call(this);
});
player.embed.on('loaded', () => {
// Assure state and events are updated on autoplay
player.embed.getPaused().then(paused => {
assurePlaybackState.call(player, !paused);
if (!paused) {
triggerEvent.call(player, player.media, 'playing');
}
});
// Get current time
player.embed.getCurrentTime().then((value) => {
currentTime = value;
triggerEvent.call(player, player.media, 'timeupdate');
});
if (is.element(player.embed.element) && player.supported.ui) {
const frame = player.embed.element;
// Get duration
player.embed.getDuration().then((value) => {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
});
// Fix keyboard focus issues
// https://github.com/sampotts/plyr/issues/317
frame.setAttribute('tabindex', -1);
}
});
// Get captions
player.embed.getTextTracks().then((tracks) => {
player.media.textTracks = tracks;
captions.setup.call(player);
});
player.embed.on('play', () => {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('cuechange', ({ cues = [] }) => {
const strippedCues = cues.map((cue) => stripHTML(cue.text));
captions.updateCues.call(player, strippedCues);
});
player.embed.on('pause', () => {
assurePlaybackState.call(player, false);
});
player.embed.on('loaded', () => {
// Assure state and events are updated on autoplay
player.embed.getPaused().then((paused) => {
assurePlaybackState.call(player, !paused);
if (!paused) {
triggerEvent.call(player, player.media, 'playing');
}
});
player.embed.on('timeupdate', data => {
player.media.seeking = false;
currentTime = data.seconds;
triggerEvent.call(player, player.media, 'timeupdate');
});
if (is.element(player.embed.element) && player.supported.ui) {
const frame = player.embed.element;
player.embed.on('progress', data => {
player.media.buffered = data.percent;
triggerEvent.call(player, player.media, 'progress');
// Fix keyboard focus issues
// https://github.com/sampotts/plyr/issues/317
frame.setAttribute('tabindex', -1);
}
});
// Check all loaded
if (parseInt(data.percent, 10) === 1) {
triggerEvent.call(player, player.media, 'canplaythrough');
}
player.embed.on('bufferstart', () => {
triggerEvent.call(player, player.media, 'waiting');
});
// Get duration as if we do it before load, it gives an incorrect value
// https://github.com/sampotts/plyr/issues/891
player.embed.getDuration().then(value => {
if (value !== player.media.duration) {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('bufferend', () => {
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('seeked', () => {
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
});
player.embed.on('play', () => {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('ended', () => {
player.media.paused = true;
triggerEvent.call(player, player.media, 'ended');
});
player.embed.on('pause', () => {
assurePlaybackState.call(player, false);
});
player.embed.on('error', detail => {
player.media.error = detail;
triggerEvent.call(player, player.media, 'error');
});
player.embed.on('timeupdate', (data) => {
player.media.seeking = false;
currentTime = data.seconds;
triggerEvent.call(player, player.media, 'timeupdate');
});
// Rebuild UI
setTimeout(() => ui.build.call(player), 0);
},
player.embed.on('progress', (data) => {
player.media.buffered = data.percent;
triggerEvent.call(player, player.media, 'progress');
// Check all loaded
if (parseInt(data.percent, 10) === 1) {
triggerEvent.call(player, player.media, 'canplaythrough');
}
// Get duration as if we do it before load, it gives an incorrect value
// https://github.com/sampotts/plyr/issues/891
player.embed.getDuration().then((value) => {
if (value !== player.media.duration) {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('seeked', () => {
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
});
player.embed.on('ended', () => {
player.media.paused = true;
triggerEvent.call(player, player.media, 'ended');
});
player.embed.on('error', (detail) => {
player.media.error = detail;
triggerEvent.call(player, player.media, 'error');
});
// Rebuild UI
if (config.customControls) {
setTimeout(() => ui.build.call(player), 0);
}
},
};
export default vimeo;

View File

@ -7,432 +7,441 @@ import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import loadImage from '../utils/loadImage';
import loadScript from '../utils/loadScript';
import loadImage from '../utils/load-image';
import loadScript from '../utils/load-script';
import { extend } from '../utils/objects';
import { format, generateId } from '../utils/strings';
import { roundAspectRatio, setAspectRatio } from '../utils/style';
// Parse YouTube ID from URL
function parseId(url) {
if (is.empty(url)) {
return null;
}
if (is.empty(url)) {
return null;
}
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
return url.match(regex) ? RegExp.$2 : url;
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
return url.match(regex) ? RegExp.$2 : url;
}
// Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) {
this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) {
this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
function getHost(config) {
if (config.noCookie) {
return 'https://www.youtube-nocookie.com';
}
if (window.location.protocol === 'http:') {
return 'http://www.youtube.com';
}
// Use YouTube's default
return undefined;
}
const youtube = {
setup() {
// Add embed class for responsive
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
setup() {
// Add embed class for responsive
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Setup API
if (is.object(window.YT) && is.function(window.YT.Player)) {
youtube.ready.call(this);
} else {
// Reference current global callback
const callback = window.onYouTubeIframeAPIReady;
// Set callback to process queue
window.onYouTubeIframeAPIReady = () => {
// Call global callback if set
if (is.function(callback)) {
callback();
}
youtube.ready.call(this);
};
// Load the SDK
loadScript(this.config.urls.youtube.sdk).catch((error) => {
this.debug.warn('YouTube API failed to load', error);
});
}
},
// Get the media title
getTitle(videoId) {
const url = format(this.config.urls.youtube.api, videoId);
fetch(url)
.then((data) => {
if (is.object(data)) {
const { title, height, width } = data;
// Set title
this.config.title = title;
ui.setTitle.call(this);
// Set aspect ratio
this.embed.ratio = roundAspectRatio(width, height);
}
setAspectRatio.call(this);
})
.catch(() => {
// Set aspect ratio
youtube.setAspectRatio.call(this);
setAspectRatio.call(this);
});
},
// Setup API
if (is.object(window.YT) && is.function(window.YT.Player)) {
youtube.ready.call(this);
} else {
// Load the API
loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
// API ready
ready() {
const player = this;
const config = player.config.youtube;
// Ignore already setup (race condition)
const currentId = player.media && player.media.getAttribute('id');
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return;
}
// Setup callback for the API
// YouTube has it's own system of course...
window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || [];
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Add to queue
window.onYouTubeReadyCallbacks.push(() => {
youtube.ready.call(this);
});
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}
// Set callback to process queue
window.onYouTubeIframeAPIReady = () => {
window.onYouTubeReadyCallbacks.forEach(callback => {
callback();
});
};
}
},
// Replace the <iframe> with a <div> due to YouTube API issues
const videoId = parseId(source);
const id = generateId(player.provider);
// Replace media element
const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });
player.media = replaceElement(container, player.media);
// Get the media title
getTitle(videoId) {
// Try via undocumented API method first
// This method disappears now and then though...
// https://github.com/sampotts/plyr/issues/709
if (is.function(this.embed.getVideoData)) {
const { title } = this.embed.getVideoData();
// Only load the poster when using custom controls
if (config.customControls) {
const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
if (is.empty(title)) {
this.config.title = title;
ui.setTitle.call(this);
return;
}
}
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
.then((image) => ui.setPoster.call(player, image.src))
.then((src) => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!src.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
}
})
.catch(() => {});
}
// 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);
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
player.embed = new window.YT.Player(player.media, {
videoId,
host: getHost(config),
playerVars: extend(
{},
{
// Autoplay
autoplay: player.config.autoplay ? 1 : 0,
// iframe interface language
hl: player.config.hl,
// Only show controls if not fully supported or opted out
controls: player.supported.ui && config.customControls ? 0 : 1,
// Disable keyboard as we handle it
disablekb: 1,
// Allow iOS inline playback
playsinline: !player.config.fullscreen.iosNative ? 1 : 0,
// Captions are flaky on YouTube
cc_load_policy: player.captions.active ? 1 : 0,
cc_lang_pref: player.config.captions.language,
// Tracking for stats
widget_referrer: window ? window.location.href : null,
},
config,
),
events: {
onError(event) {
// YouTube may fire onError twice, so only handle it once
if (!player.media.error) {
const code = event.data;
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
const message =
{
2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
101: 'The owner of the requested video does not allow it to be played in embedded players.',
150: 'The owner of the requested video does not allow it to be played in embedded players.',
}[code] || 'An unknown error occured';
fetch(url)
.then(result => {
if (is.object(result)) {
this.config.title = result.items[0].snippet.title;
ui.setTitle.call(this);
}
})
.catch(() => {});
}
},
player.media.error = { code, message };
// Set aspect ratio
setAspectRatio() {
const ratio = this.config.ratio.split(':');
this.elements.wrapper.style.paddingBottom = `${100 / ratio[0] * ratio[1]}%`;
},
triggerEvent.call(player, player.media, 'error');
}
},
onPlaybackRateChange(event) {
// Get the instance
const instance = event.target;
// API ready
ready() {
const player = this;
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
// Ignore already setup (race condition)
const currentId = player.media.getAttribute('id');
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
triggerEvent.call(player, player.media, 'ratechange');
},
onReady(event) {
// Bail if onReady has already been called. See issue #1108
if (is.function(player.media.play)) {
return;
}
}
// Get the instance
const instance = event.target;
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get the title
youtube.getTitle.call(player, videoId);
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}
// Create a faux HTML5 API using the YouTube API
player.media.play = () => {
assurePlaybackState.call(player, true);
instance.playVideo();
};
// Replace the <iframe> with a <div> due to YouTube API issues
const videoId = parseId(source);
const id = generateId(player.provider);
player.media.pause = () => {
assurePlaybackState.call(player, false);
instance.pauseVideo();
};
// Get poster, if already set
const { poster } = player;
player.media.stop = () => {
instance.stopVideo();
};
// Replace media element
const container = createElement('div', { id, poster });
player.media = replaceElement(container, player.media);
player.media.duration = instance.getDuration();
player.media.paused = true;
// Id to poster wrapper
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
// Seeking
player.media.currentTime = 0;
Object.defineProperty(player.media, 'currentTime', {
get() {
return Number(instance.getCurrentTime());
},
set(time) {
// If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
if (player.paused && !player.embed.hasPlayed) {
player.embed.mute();
}
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
.then(image => ui.setPoster.call(player, image.src))
.then(posterSrc => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!posterSrc.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
// Set seeking state and trigger event
player.media.seeking = true;
triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
},
});
// Playback speed
Object.defineProperty(player.media, 'playbackRate', {
get() {
return instance.getPlaybackRate();
},
set(input) {
instance.setPlaybackRate(input);
},
});
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
get() {
return volume;
},
set(input) {
volume = input;
instance.setVolume(volume * 100);
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Muted
let { muted } = player.config;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
instance.setVolume(volume * 100);
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Source
Object.defineProperty(player.media, 'currentSrc', {
get() {
return instance.getVideoUrl();
},
});
// Ended
Object.defineProperty(player.media, 'ended', {
get() {
return player.currentTime === player.duration;
},
});
// Get available speeds
const speeds = instance.getAvailablePlaybackRates();
// Filter based on config
player.options.speed = speeds.filter((s) => player.config.speed.options.includes(s));
// Set the tabindex to avoid focus entering iframe
if (player.supported.ui && config.customControls) {
player.media.setAttribute('tabindex', -1);
}
triggerEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'durationchange');
// Reset timer
clearInterval(player.timers.buffering);
// Setup buffering
player.timers.buffering = setInterval(() => {
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
// Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
triggerEvent.call(player, player.media, 'progress');
}
// Set last buffer point
player.media.lastBuffered = player.media.buffered;
// Bail if we're at 100%
if (player.media.buffered === 1) {
clearInterval(player.timers.buffering);
// Trigger event
triggerEvent.call(player, player.media, 'canplaythrough');
}
}, 200);
// Rebuild UI
if (config.customControls) {
setTimeout(() => ui.build.call(player), 50);
}
},
onStateChange(event) {
// Get the instance
const instance = event.target;
// Reset timer
clearInterval(player.timers.playing);
const seeked = player.media.seeking && [1, 2].includes(event.data);
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
}
// Handle events
// -1 Unstarted
// 0 Ended
// 1 Playing
// 2 Paused
// 3 Buffering
// 5 Video cued
switch (event.data) {
case -1:
// Update scrubber
triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
triggerEvent.call(player, player.media, 'progress');
break;
case 0:
assurePlaybackState.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) {
// YouTube needs a call to `stopVideo` before playing again
instance.stopVideo();
instance.playVideo();
} else {
triggerEvent.call(player, player.media, 'ended');
}
break;
case 1:
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
player.media.pause();
} else {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
triggerEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
// https://github.com/sampotts/plyr/issues/374
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
triggerEvent.call(player, player.media, 'durationchange');
}
})
.catch(() => {});
}
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
player.embed = new window.YT.Player(id, {
videoId,
playerVars: {
autoplay: player.config.autoplay ? 1 : 0, // Autoplay
hl: player.config.hl, // iframe interface language
controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported
rel: 0, // No related vids
showinfo: 0, // Hide info
iv_load_policy: 3, // Hide annotations
modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)
disablekb: 1, // Disable keyboard as we handle it
playsinline: 1, // Allow iOS inline playback
break;
// Tracking for stats
// origin: window ? `${window.location.protocol}//${window.location.host}` : null,
widget_referrer: window ? window.location.href : null,
case 2:
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.muted) {
player.embed.unMute();
}
assurePlaybackState.call(player, false);
// Captions are flaky on YouTube
cc_load_policy: player.captions.active ? 1 : 0,
cc_lang_pref: player.config.captions.language,
},
events: {
onError(event) {
// YouTube may fire onError twice, so only handle it once
if (!player.media.error) {
const code = event.data;
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
const message =
{
2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
101: 'The owner of the requested video does not allow it to be played in embedded players.',
150: 'The owner of the requested video does not allow it to be played in embedded players.',
}[code] || 'An unknown error occured';
break;
player.media.error = { code, message };
case 3:
// Trigger waiting event to add loading classes to container as the video buffers.
triggerEvent.call(player, player.media, 'waiting');
triggerEvent.call(player, player.media, 'error');
}
},
onPlaybackRateChange(event) {
// Get the instance
const instance = event.target;
break;
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
default:
break;
}
triggerEvent.call(player, player.media, 'ratechange');
},
onReady(event) {
// Bail if onReady has already been called. See issue #1108
if (is.function(player.media.play)) {
return;
}
// Get the instance
const instance = event.target;
// Get the title
youtube.getTitle.call(player, videoId);
// Create a faux HTML5 API using the YouTube API
player.media.play = () => {
assurePlaybackState.call(player, true);
instance.playVideo();
};
player.media.pause = () => {
assurePlaybackState.call(player, false);
instance.pauseVideo();
};
player.media.stop = () => {
instance.stopVideo();
};
player.media.duration = instance.getDuration();
player.media.paused = true;
// Seeking
player.media.currentTime = 0;
Object.defineProperty(player.media, 'currentTime', {
get() {
return Number(instance.getCurrentTime());
},
set(time) {
// If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
if (player.paused && !player.embed.hasPlayed) {
player.embed.mute();
}
// Set seeking state and trigger event
player.media.seeking = true;
triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
},
});
// Playback speed
Object.defineProperty(player.media, 'playbackRate', {
get() {
return instance.getPlaybackRate();
},
set(input) {
instance.setPlaybackRate(input);
},
});
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
get() {
return volume;
},
set(input) {
volume = input;
instance.setVolume(volume * 100);
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Muted
let { muted } = player.config;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Source
Object.defineProperty(player.media, 'currentSrc', {
get() {
return instance.getVideoUrl();
},
});
// Ended
Object.defineProperty(player.media, 'ended', {
get() {
return player.currentTime === player.duration;
},
});
// Get available speeds
player.options.speed = instance.getAvailablePlaybackRates();
// Set the tabindex to avoid focus entering iframe
if (player.supported.ui) {
player.media.setAttribute('tabindex', -1);
}
triggerEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'durationchange');
// Reset timer
clearInterval(player.timers.buffering);
// Setup buffering
player.timers.buffering = setInterval(() => {
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
// Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
triggerEvent.call(player, player.media, 'progress');
}
// Set last buffer point
player.media.lastBuffered = player.media.buffered;
// Bail if we're at 100%
if (player.media.buffered === 1) {
clearInterval(player.timers.buffering);
// Trigger event
triggerEvent.call(player, player.media, 'canplaythrough');
}
}, 200);
// Rebuild UI
setTimeout(() => ui.build.call(player), 50);
},
onStateChange(event) {
// Get the instance
const instance = event.target;
// Reset timer
clearInterval(player.timers.playing);
const seeked = player.media.seeking && [1, 2].includes(event.data);
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
}
// Handle events
// -1 Unstarted
// 0 Ended
// 1 Playing
// 2 Paused
// 3 Buffering
// 5 Video cued
switch (event.data) {
case -1:
// Update scrubber
triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
triggerEvent.call(player, player.media, 'progress');
break;
case 0:
assurePlaybackState.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) {
// YouTube needs a call to `stopVideo` before playing again
instance.stopVideo();
instance.playVideo();
} else {
triggerEvent.call(player, player.media, 'ended');
}
break;
case 1:
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (player.media.paused && !player.embed.hasPlayed) {
player.media.pause();
} else {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
triggerEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
// https://github.com/sampotts/plyr/issues/374
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
triggerEvent.call(player, player.media, 'durationchange');
}
}
break;
case 2:
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.muted) {
player.embed.unMute();
}
assurePlaybackState.call(player, false);
break;
default:
break;
}
triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},
},
});
},
triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},
},
});
},
};
export default youtube;

679
src/js/plyr.d.ts vendored Normal file
View File

@ -0,0 +1,679 @@
// Type definitions for plyr 3.5
// Project: https://plyr.io
// Definitions by: ondratra <https://github.com/ondratra>
// TypeScript Version: 3.0
export = Plyr;
export as namespace Plyr;
declare class Plyr {
/**
* Setup a new instance
*/
static setup(targets: NodeList | HTMLElement | HTMLElement[] | string, options?: Plyr.Options): Plyr[];
/**
* Check for support
* @param mediaType
* @param provider
* @param playsInline Whether the player has the playsinline attribute (only applicable to iOS 10+)
*/
static supported(mediaType?: Plyr.MediaType, provider?: Plyr.Provider, playsInline?: boolean): Plyr.Support;
constructor(targets: NodeList | HTMLElement | HTMLElement[] | string, options?: Plyr.Options);
/**
* Indicates if the current player is HTML5.
*/
readonly isHTML5: boolean;
/**
* Indicates if the current player is an embedded player.
*/
readonly isEmbed: boolean;
/**
* Indicates if the current player is playing.
*/
readonly playing: boolean;
/**
* Indicates if the current player is paused.
*/
readonly paused: boolean;
/**
* Indicates if the current player is stopped.
*/
readonly stopped: boolean;
/**
* Indicates if the current player has finished playback.
*/
readonly ended: boolean;
/**
* Returns a float between 0 and 1 indicating how much of the media is buffered
*/
readonly buffered: number;
/**
* Gets or sets the currentTime for the player. The setter accepts a float in seconds.
*/
currentTime: number;
/**
* Indicates if the current player is seeking.
*/
readonly seeking: boolean;
/**
* Returns the duration for the current media.
*/
readonly duration: number;
/**
* Gets or sets the volume for the player. The setter accepts a float between 0 and 1.
*/
volume: number;
/**
* Gets or sets the muted state of the player. The setter accepts a boolean.
*/
muted: boolean;
/**
* Indicates if the current media has an audio track.
*/
readonly hasAudio: boolean;
/**
* Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5.
*/
speed: number;
/**
* Gets or sets the quality for the player. The setter accepts a value from the options specified in your config.
*/
quality: number;
/**
* Gets or sets the current loop state of the player.
*/
loop: boolean;
/**
* Gets or sets the current source for the player.
*/
source: Plyr.SourceInfo;
/**
* Gets or sets the current poster image URL for the player.
*/
poster: string;
/**
* Gets or sets the autoplay state of the player.
*/
autoplay: boolean;
/**
* Gets or sets the caption track by index. 1 means the track is missing or captions is not active
*/
currentTrack: number;
/**
* Gets or sets the preferred captions language for the player. The setter accepts an ISO twoletter language code. Support for the languages is dependent on the captions you include.
* If your captions don't have any language data, or if you have multiple tracks with the same language, you may want to use currentTrack instead.
*/
language: string;
/**
* Gets or sets the picture-in-picture state of the player. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+.
*/
pip: boolean;
/**
* Gets or sets the aspect ratio for embedded players.
*/
ratio?: string;
/**
* Access Elements cache
*/
elements: Plyr.Elements;
/**
* Returns the current video Provider
*/
readonly provider: Plyr.Provider;
/**
* Returns the native API for Vimeo or Youtube players
*/
readonly embed?: any;
readonly fullscreen: Plyr.FullscreenControl;
/**
* Start playback.
* For HTML5 players, play() will return a Promise in some browsers - WebKit and Mozilla according to MDN at time of writing.
*/
play(): Promise<void> | void;
/**
* Pause playback.
*/
pause(): void;
/**
* Toggle playback, if no parameters are passed, it will toggle based on current status.
*/
togglePlay(toggle?: boolean): boolean;
/**
* Stop playback and reset to start.
*/
stop(): void;
/**
* Restart playback.
*/
restart(): void;
/**
* Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used.
*/
rewind(seekTime?: number): void;
/**
* Fast forward by the specified seek time. If no parameter is passed, the default seek time will be used.
*/
forward(seekTime?: number): void;
/**
* Increase volume by the specified step. If no parameter is passed, the default step will be used.
*/
increaseVolume(step?: number): void;
/**
* Increase volume by the specified step. If no parameter is passed, the default step will be used.
*/
decreaseVolume(step?: number): void;
/**
* Toggle captions display. If no parameter is passed, it will toggle based on current status.
*/
toggleCaptions(toggle?: boolean): void;
/**
* Trigger the airplay dialog on supported devices.
*/
airplay(): void;
/**
* Toggle the controls (video only). Takes optional truthy value to force it on/off.
*/
toggleControls(toggle: boolean): void;
/**
* Add an event listener for the specified event.
*/
on<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
/**
* Add an event listener for the specified event once.
*/
once<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
/**
* Remove an event listener for the specified event.
*/
off<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
/**
* Check support for a mime type.
*/
supports(type: string): boolean;
/**
* Destroy lib instance
*/
destroy(): void;
}
declare namespace Plyr {
type MediaType = 'audio' | 'video';
type Provider = 'html5' | 'youtube' | 'vimeo';
type StandardEventMap = {
progress: PlyrEvent;
playing: PlyrEvent;
play: PlyrEvent;
pause: PlyrEvent;
timeupdate: PlyrEvent;
volumechange: PlyrEvent;
seeking: PlyrEvent;
seeked: PlyrEvent;
ratechange: PlyrEvent;
ended: PlyrEvent;
enterfullscreen: PlyrEvent;
exitfullscreen: PlyrEvent;
captionsenabled: PlyrEvent;
captionsdisabled: PlyrEvent;
languagechange: PlyrEvent;
controlshidden: PlyrEvent;
controlsshown: PlyrEvent;
ready: PlyrEvent;
};
// For retrocompatibility, we keep StandadEvent
type StandadEvent = keyof Plyr.StandardEventMap;
type Html5EventMap = {
loadstart: PlyrEvent;
loadeddata: PlyrEvent;
loadedmetadata: PlyrEvent;
canplay: PlyrEvent;
canplaythrough: PlyrEvent;
stalled: PlyrEvent;
waiting: PlyrEvent;
emptied: PlyrEvent;
cuechange: PlyrEvent;
error: PlyrEvent;
};
// For retrocompatibility, we keep Html5Event
type Html5Event = keyof Plyr.Html5EventMap;
type YoutubeEventMap = {
statechange: PlyrStateChangeEvent;
qualitychange: PlyrEvent;
qualityrequested: PlyrEvent;
};
// For retrocompatibility, we keep YoutubeEvent
type YoutubeEvent = keyof Plyr.YoutubeEventMap;
type PlyrEventMap = StandardEventMap & Html5EventMap & YoutubeEventMap;
interface FullscreenControl {
/**
* Indicates if the current player is in fullscreen mode.
*/
readonly active: boolean;
/**
* Indicates if the current player has fullscreen enabled.
*/
readonly enabled: boolean;
/**
* Enter fullscreen. If fullscreen is not supported, a fallback ""full window/viewport"" is used instead.
*/
enter(): void;
/**
* Exit fullscreen.
*/
exit(): void;
/**
* Toggle fullscreen.
*/
toggle(): void;
}
interface Options {
/**
* Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below.
*/
enabled?: boolean;
/**
* Display debugging information in the console
*/
debug?: boolean;
/**
* If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function;
* id (the unique id for the player), seektime (the seektime step in seconds), and title (the media title). See CONTROLS.md for more info on how the html needs to be structured.
* Defaults to ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']
*/
controls?: string[] | ((id: string, seektime: number, title: string) => unknown) | Element;
/**
* If you're using the default controls are used then you can specify which settings to show in the menu
* Defaults to ['captions', 'quality', 'speed', 'loop']
*/
settings?: string[];
/**
* Used for internationalization (i18n) of the text within the UI.
*/
i18n?: any;
/**
* Load the SVG sprite specified as the iconUrl option (if a URL). If false, it is assumed you are handling sprite loading yourself.
*/
loadSprite?: boolean;
/**
* Specify a URL or path to the SVG sprite. See the SVG section for more info.
*/
iconUrl?: string;
/**
* Specify the id prefix for the icons used in the default controls (e.g. plyr-play would be plyr).
* This is to prevent clashes if you're using your own SVG sprite but with the default controls.
* Most people can ignore this option.
*/
iconPrefix?: string;
/**
* Specify a URL or path to a blank video file used to properly cancel network requests.
*/
blankVideo?: string;
/**
* Autoplay the media on load. This is generally advised against on UX grounds. It is also disabled by default in some browsers.
* If the autoplay attribute is present on a <video> or <audio> element, this will be automatically set to true.
*/
autoplay?: boolean;
/**
* Only allow one player playing at once.
*/
autopause?: boolean;
/**
* The time, in seconds, to seek when a user hits fast forward or rewind.
*/
seekTime?: number;
/**
* A number, between 0 and 1, representing the initial volume of the player.
*/
volume?: number;
/**
* Whether to start playback muted. If the muted attribute is present on a <video> or <audio> element, this will be automatically set to true.
*/
muted?: boolean;
/**
* Click (or tap) of the video container will toggle play/pause.
*/
clickToPlay?: boolean;
/**
* Disable right click menu on video to help as very primitive obfuscation to prevent downloads of content.
*/
disableContextMenu?: boolean;
/**
* Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen.
* As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly.
*/
hideControls?: boolean;
/**
* Reset the playback to the start once playback is complete.
*/
resetOnEnd?: boolean;
/**
* Enable keyboard shortcuts for focused players only or globally
*/
keyboard?: KeyboardOptions;
/**
* controls: Display control labels as tooltips on :hover & :focus (by default, the labels are screen reader only).
* seek: Display a seek tooltip to indicate on click where the media would seek to.
*/
tooltips?: TooltipOptions;
/**
* Specify a custom duration for media.
*/
duration?: number;
/**
* Displays the duration of the media on the metadataloaded event (on startup) in the current time display.
* This will only work if the preload attribute is not set to none (or is not set at all) and you choose not to display the duration (see controls option).
*/
displayDuration?: boolean;
/**
* Display the current time as a countdown rather than an incremental counter.
*/
invertTime?: boolean;
/**
* Allow users to click to toggle the above.
*/
toggleInvert?: boolean;
/**
* Allows binding of event listeners to the controls before the default handlers. See the defaults.js for available listeners.
* If your handler prevents default on the event (event.preventDefault()), the default handler will not fire.
*/
listeners?: { [key: string]: (error: PlyrEvent) => void };
/**
* active: Toggles if captions should be active by default. language: Sets the default language to load (if available). 'auto' uses the browser language.
* update: Listen to changes to tracks and update menu. This is needed for some streaming libraries, but can result in unselectable language options).
*/
captions?: CaptionOptions;
/**
* enabled: Toggles whether fullscreen should be enabled. fallback: Allow fallback to a full-window solution.
* iosNative: whether to use native iOS fullscreen when entering fullscreen (no custom controls)
*/
fullscreen?: FullScreenOptions;
/**
* The aspect ratio you want to use for embedded players.
*/
ratio?: string;
/**
* enabled: Allow use of local storage to store user settings. key: The key name to use.
*/
storage?: StorageOptions;
/**
* selected: The default speed for playback. options: The speed options to display in the UI. YouTube and Vimeo will ignore any options outside of the 0.5-2 range, so options outside of this range will be hidden automatically.
*/
speed?: SpeedOptions;
/**
* Currently only supported by YouTube. default is the default quality level, determined by YouTube. options are the options to display.
*/
quality?: QualityOptions;
/**
* active: Whether to loop the current video. If the loop attribute is present on a <video> or <audio> element,
* this will be automatically set to true This is an object to support future functionality.
*/
loop?: LoopOptions;
/**
* enabled: Whether to enable vi.ai ads. publisherId: Your unique vi.ai publisher ID.
*/
ads?: AdOptions;
/**
* Vimeo Player Options.
*/
vimeo?: object;
/**
* Youtube Player Options.
*/
youtube?: object;
/**
* Preview Thumbnails Options.
*/
previewThumbnails?: PreviewThumbnailsOptions;
}
interface QualityOptions {
default: number;
forced?: boolean;
onChange?: (quality: number) => void;
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 | 'force';
allowAudio?: boolean;
iosNative?: boolean;
}
interface CaptionOptions {
active?: boolean;
language?: string;
update?: boolean;
}
interface StorageOptions {
enabled?: boolean;
key?: string;
}
interface PreviewThumbnailsOptions {
enabled?: boolean;
src?: string | string[];
}
export interface Elements {
buttons: {
airplay?: HTMLButtonElement;
captions?: HTMLButtonElement;
download?: HTMLButtonElement;
fastForward?: HTMLButtonElement;
fullscreen?: HTMLButtonElement;
mute?: HTMLButtonElement;
pip?: HTMLButtonElement;
play?: HTMLButtonElement | HTMLButtonElement[];
restart?: HTMLButtonElement;
rewind?: HTMLButtonElement;
settings?: HTMLButtonElement;
};
captions: HTMLElement | null;
container: HTMLElement | null;
controls: HTMLElement | null;
fullscreen: HTMLElement | null;
wrapper: HTMLElement | null;
}
interface SourceInfo {
/**
* Note: YouTube and Vimeo are currently not supported as audio sources.
*/
type: MediaType;
/**
* Title of the new media. Used for the aria-label attribute on the play button, and outer container. YouTube and Vimeo are populated automatically.
*/
title?: string;
/**
* This is an array of sources. For HTML5 media, the properties of this object are mapped directly to HTML attributes so more can be added to the object if required.
*/
sources: Source[];
/**
* The URL for the poster image (HTML5 video only).
*/
poster?: string;
/**
* An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above,
* it will render as <track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default> and similar for the French version.
* Booleans are converted to HTML5 value-less attributes.
*/
tracks?: Track[];
}
interface Source {
/**
* The URL of the media file (or YouTube/Vimeo URL).
*/
src: string;
/**
* The MIME type of the media file (if HTML5).
*/
type?: string;
provider?: Provider;
size?: number;
}
type TrackKind = 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
interface Track {
/**
* Indicates how the text track is meant to be used
*/
kind: TrackKind;
/**
* Indicates a user-readable title for the track
*/
label: string;
/**
* The language of the track text data. It must be a valid BCP 47 language tag. If the kind attribute is set to subtitles, then srclang must be defined.
*/
srcLang?: string;
/**
* The URL of the track (.vtt file).
*/
src: string;
default?: boolean;
}
interface PlyrEvent extends CustomEvent {
readonly detail: { readonly plyr: Plyr };
}
enum YoutubeState {
UNSTARTED = -1,
ENDED = 0,
PLAYING = 1,
PAUSED = 2,
BUFFERING = 3,
CUED = 5,
}
interface PlyrStateChangeEvent extends CustomEvent {
readonly detail: {
readonly plyr: Plyr;
readonly code: YoutubeState;
};
}
interface Support {
api: boolean;
ui: boolean;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,13 @@
// ==========================================================================
// Plyr Polyfilled Build
// plyr.js v3.4.5
// plyr.js v3.6.7
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import 'custom-event-polyfill';
import 'url-polyfill';
import Plyr from './plyr';
export default Plyr;

View File

@ -5,6 +5,7 @@
import { providers } from './config/types';
import html5 from './html5';
import media from './media';
import PreviewThumbnails from './plugins/preview-thumbnails';
import support from './support';
import ui from './ui';
import { createElement, insertElement, removeElement } from './utils/elements';
@ -12,128 +13,146 @@ import is from './utils/is';
import { getDeep } from './utils/objects';
const source = {
// Add elements to HTML5 media (source, tracks, etc)
insertElements(type, attributes) {
if (is.string(attributes)) {
insertElement(type, this.media, {
src: attributes,
});
} else if (is.array(attributes)) {
attributes.forEach(attribute => {
insertElement(type, this.media, attribute);
});
}
},
// Add elements to HTML5 media (source, tracks, etc)
insertElements(type, attributes) {
if (is.string(attributes)) {
insertElement(type, this.media, {
src: attributes,
});
} else if (is.array(attributes)) {
attributes.forEach((attribute) => {
insertElement(type, this.media, attribute);
});
}
},
// Update source
// Sources are not checked for support so be careful
change(input) {
if (!getDeep(input, 'sources.length')) {
this.debug.warn('Invalid source format');
return;
// Update source
// Sources are not checked for support so be careful
change(input) {
if (!getDeep(input, 'sources.length')) {
this.debug.warn('Invalid source format');
return;
}
// Cancel current network requests
html5.cancelRequests.call(this);
// Destroy instance and re-setup
this.destroy.call(
this,
() => {
// Reset quality options
this.options.quality = [];
// Remove elements
removeElement(this.media);
this.media = null;
// Reset class name
if (is.element(this.elements.container)) {
this.elements.container.removeAttribute('class');
}
// Cancel current network requests
html5.cancelRequests.call(this);
// Set the type and provider
const { sources, type } = input;
const [{ provider = providers.html5, src }] = sources;
const tagName = provider === 'html5' ? type : 'div';
const attributes = provider === 'html5' ? {} : { src };
// Destroy instance and re-setup
this.destroy.call(
this,
() => {
// Reset quality options
this.options.quality = [];
Object.assign(this, {
provider,
type,
// Check for support
supported: support.check(type, provider, this.config.playsinline),
// Create new element
media: createElement(tagName, attributes),
});
// Remove elements
removeElement(this.media);
this.media = null;
// Inject the new element
this.elements.container.appendChild(this.media);
// Reset class name
if (is.element(this.elements.container)) {
this.elements.container.removeAttribute('class');
}
// Autoplay the new source?
if (is.boolean(input.autoplay)) {
this.config.autoplay = input.autoplay;
}
// Set the type and provider
const { sources, type } = input;
const [{ provider = providers.html5, src }] = sources;
const tagName = provider === 'html5' ? type : 'div';
const attributes = provider === 'html5' ? {} : { src };
// Set attributes for audio and video
if (this.isHTML5) {
if (this.config.crossorigin) {
this.media.setAttribute('crossorigin', '');
}
if (this.config.autoplay) {
this.media.setAttribute('autoplay', '');
}
if (!is.empty(input.poster)) {
this.poster = input.poster;
}
if (this.config.loop.active) {
this.media.setAttribute('loop', '');
}
if (this.config.muted) {
this.media.setAttribute('muted', '');
}
if (this.config.playsinline) {
this.media.setAttribute('playsinline', '');
}
}
Object.assign(this, {
provider,
type,
// Check for support
supported: support.check(type, provider, this.config.playsinline),
// Create new element
media: createElement(tagName, attributes),
});
// Restore class hook
ui.addStyleHook.call(this);
// Inject the new element
this.elements.container.appendChild(this.media);
// Set new sources for html5
if (this.isHTML5) {
source.insertElements.call(this, 'source', sources);
}
// Autoplay the new source?
if (is.boolean(input.autoplay)) {
this.config.autoplay = input.autoplay;
}
// Set video title
this.config.title = input.title;
// Set attributes for audio and video
if (this.isHTML5) {
if (this.config.crossorigin) {
this.media.setAttribute('crossorigin', '');
}
if (this.config.autoplay) {
this.media.setAttribute('autoplay', '');
}
if (!is.empty(input.poster)) {
this.poster = input.poster;
}
if (this.config.loop.active) {
this.media.setAttribute('loop', '');
}
if (this.config.muted) {
this.media.setAttribute('muted', '');
}
if (this.config.playsinline) {
this.media.setAttribute('playsinline', '');
}
}
// Set up from scratch
media.setup.call(this);
// Restore class hook
ui.addStyleHook.call(this);
// HTML5 stuff
if (this.isHTML5) {
// Setup captions
if (Object.keys(input).includes('tracks')) {
source.insertElements.call(this, 'track', input.tracks);
}
}
// Set new sources for html5
if (this.isHTML5) {
source.insertElements.call(this, 'source', sources);
}
// If HTML5 or embed but not fully supported, setupInterface and call ready now
if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {
// Setup interface
ui.build.call(this);
}
// Set video title
this.config.title = input.title;
// Load HTML5 sources
if (this.isHTML5) {
this.media.load();
}
// Set up from scratch
media.setup.call(this);
// Update previewThumbnails config & reload plugin
if (!is.empty(input.previewThumbnails)) {
Object.assign(this.config.previewThumbnails, input.previewThumbnails);
// HTML5 stuff
if (this.isHTML5) {
// Setup captions
if ('tracks' in input) {
source.insertElements.call(this, 'track', input.tracks);
}
// Cleanup previewThumbnails plugin if it was loaded
if (this.previewThumbnails && this.previewThumbnails.loaded) {
this.previewThumbnails.destroy();
this.previewThumbnails = null;
}
// Load HTML5 sources
this.media.load();
}
// Create new instance if it is still enabled
if (this.config.previewThumbnails.enabled) {
this.previewThumbnails = new PreviewThumbnails(this);
}
}
// If HTML5 or embed but not fully supported, setupInterface and call ready now
if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {
// Setup interface
ui.build.call(this);
}
// Update the fullscreen support
this.fullscreen.update();
},
true,
);
},
// Update the fullscreen support
this.fullscreen.update();
},
true,
);
},
};
export default source;

View File

@ -6,72 +6,72 @@ import is from './utils/is';
import { extend } from './utils/objects';
class Storage {
constructor(player) {
this.enabled = player.config.storage.enabled;
this.key = player.config.storage.key;
constructor(player) {
this.enabled = player.config.storage.enabled;
this.key = player.config.storage.key;
}
// Check for actual support (see if we can use it)
static get supported() {
try {
if (!('localStorage' in window)) {
return false;
}
const test = '___test';
// Try to use it (it might be disabled, e.g. user is in private mode)
// see: https://github.com/sampotts/plyr/issues/131
window.localStorage.setItem(test, test);
window.localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
get = (key) => {
if (!Storage.supported || !this.enabled) {
return null;
}
// Check for actual support (see if we can use it)
static get supported() {
try {
if (!('localStorage' in window)) {
return false;
}
const store = window.localStorage.getItem(this.key);
const test = '___test';
// Try to use it (it might be disabled, e.g. user is in private mode)
// see: https://github.com/sampotts/plyr/issues/131
window.localStorage.setItem(test, test);
window.localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
if (is.empty(store)) {
return null;
}
get(key) {
if (!Storage.supported || !this.enabled) {
return null;
}
const json = JSON.parse(store);
const store = window.localStorage.getItem(this.key);
return is.string(key) && key.length ? json[key] : json;
};
if (is.empty(store)) {
return null;
}
const json = JSON.parse(store);
return is.string(key) && key.length ? json[key] : json;
set = (object) => {
// Bail if we don't have localStorage support or it's disabled
if (!Storage.supported || !this.enabled) {
return;
}
set(object) {
// Bail if we don't have localStorage support or it's disabled
if (!Storage.supported || !this.enabled) {
return;
}
// Can only store objectst
if (!is.object(object)) {
return;
}
// Get current storage
let storage = this.get();
// Default to empty object
if (is.empty(storage)) {
storage = {};
}
// Update the working copy of the values
extend(storage, object);
// Update storage
window.localStorage.setItem(this.key, JSON.stringify(storage));
// Can only store objectst
if (!is.object(object)) {
return;
}
// Get current storage
let storage = this.get();
// Default to empty object
if (is.empty(storage)) {
storage = {};
}
// Update the working copy of the values
extend(storage, object);
// Update storage
window.localStorage.setItem(this.key, JSON.stringify(storage));
};
}
export default Storage;

View File

@ -9,92 +9,110 @@ import is from './utils/is';
// Default codecs for checking mimetype support
const defaultCodecs = {
'audio/ogg': 'vorbis',
'audio/wav': '1',
'video/webm': 'vp8, vorbis',
'video/mp4': 'avc1.42E01E, mp4a.40.2',
'video/ogg': 'theora',
'audio/ogg': 'vorbis',
'audio/wav': '1',
'video/webm': 'vp8, vorbis',
'video/mp4': 'avc1.42E01E, mp4a.40.2',
'video/ogg': 'theora',
};
// Check for feature support
const support = {
// Basic support
audio: 'canPlayType' in document.createElement('audio'),
video: 'canPlayType' in document.createElement('video'),
// Basic support
audio: 'canPlayType' in document.createElement('audio'),
video: 'canPlayType' in document.createElement('video'),
// Check for support
// Basic functionality vs full UI
check(type, provider, playsinline) {
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
const api = support[type] || provider !== 'html5';
const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
// Check for support
// Basic functionality vs full UI
check(type, provider, playsinline) {
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
const api = support[type] || provider !== 'html5';
const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
return {
api,
ui,
};
},
return {
api,
ui,
};
},
// Picture-in-picture support
// Safari only currently
pip: (() => !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode))(),
// Picture-in-picture support
// Safari & Chrome only currently
pip: (() => {
if (browser.isIPhone) {
return false;
}
// Airplay support
// Safari only currently
airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
// Safari
// https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls
if (is.function(createElement('video').webkitSetPresentationMode)) {
return true;
}
// Inline playback support
// https://webkit.org/blog/6784/new-video-policies-for-ios/
playsinline: 'playsInline' in document.createElement('video'),
// Chrome
// https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture
if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {
return true;
}
// Check for mime type support against a player instance
// Credits: http://diveintohtml5.info/everything.html
// Related: http://www.leanbackplayer.com/test/h5mt.html
mime(inputType) {
const [mediaType] = inputType.split('/');
if (!this.isHTML5 || mediaType !== this.type) {
return false;
}
return false;
})(),
let type;
if (inputType && inputType.includes('codecs=')) {
// Use input directly
type = inputType;
} else if (inputType === 'audio/mpeg') {
// Skip codec
type = 'audio/mpeg;';
} else if (inputType in defaultCodecs) {
// Use codec
type = `${inputType}; codecs="${defaultCodecs[inputType]}"`;
}
// Airplay support
// Safari only currently
airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
try {
return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
} catch (err) {
return false;
}
},
// Inline playback support
// https://webkit.org/blog/6784/new-video-policies-for-ios/
playsinline: 'playsInline' in document.createElement('video'),
// Check for textTracks support
textTracks: 'textTracks' in document.createElement('video'),
// Check for mime type support against a player instance
// Credits: http://diveintohtml5.info/everything.html
// Related: http://www.leanbackplayer.com/test/h5mt.html
mime(input) {
if (is.empty(input)) {
return false;
}
// <input type="range"> Sliders
rangeInput: (() => {
const range = document.createElement('input');
range.type = 'range';
return range.type === 'range';
})(),
const [mediaType] = input.split('/');
let type = input;
// Touch
// NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
touch: 'ontouchstart' in document.documentElement,
// Verify we're using HTML5 and there's no media type mismatch
if (!this.isHTML5 || mediaType !== this.type) {
return false;
}
// Detect transitions support
transitions: transitionEndEvent !== false,
// Add codec if required
if (Object.keys(defaultCodecs).includes(type)) {
type += `; codecs="${defaultCodecs[input]}"`;
}
// Reduced motion iOS & MacOS setting
// https://webkit.org/blog/7551/responsive-design-for-motion/
reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,
try {
return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
} catch (e) {
return false;
}
},
// Check for textTracks support
textTracks: 'textTracks' in document.createElement('video'),
// <input type="range"> Sliders
rangeInput: (() => {
const range = document.createElement('input');
range.type = 'range';
return range.type === 'range';
})(),
// Touch
// NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
touch: 'ontouchstart' in document.documentElement,
// Detect transitions support
transitions: transitionEndEvent !== false,
// Reduced motion iOS & MacOS setting
// https://webkit.org/blog/7551/responsive-design-for-motion/
reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,
};
export default support;

View File

@ -10,250 +10,283 @@ import { getElement, toggleClass } from './utils/elements';
import { ready, triggerEvent } from './utils/events';
import i18n from './utils/i18n';
import is from './utils/is';
import loadImage from './utils/loadImage';
import loadImage from './utils/load-image';
const ui = {
addStyleHook() {
toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
},
addStyleHook() {
toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
},
// Toggle native HTML5 media controls
toggleNativeControls(toggle = false) {
if (toggle && this.isHTML5) {
this.media.setAttribute('controls', '');
} else {
this.media.removeAttribute('controls');
}
},
// Toggle native HTML5 media controls
toggleNativeControls(toggle = false) {
if (toggle && this.isHTML5) {
this.media.setAttribute('controls', '');
} else {
this.media.removeAttribute('controls');
}
},
// Setup the UI
build() {
// Re-attach media element listeners
// TODO: Use event bubbling?
this.listeners.media();
// Setup the UI
build() {
// Re-attach media element listeners
// TODO: Use event bubbling?
this.listeners.media();
// Don't setup interface if no support
if (!this.supported.ui) {
this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
// Don't setup interface if no support
if (!this.supported.ui) {
this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
// Restore native controls
ui.toggleNativeControls.call(this, true);
// Restore native controls
ui.toggleNativeControls.call(this, true);
// Bail
return;
}
// Bail
return;
}
// Inject custom controls if not present
if (!is.element(this.elements.controls)) {
// Inject custom controls
controls.inject.call(this);
// Inject custom controls if not present
if (!is.element(this.elements.controls)) {
// Inject custom controls
controls.inject.call(this);
// Re-attach control listeners
this.listeners.controls();
}
// Re-attach control listeners
this.listeners.controls();
}
// Remove native controls
ui.toggleNativeControls.call(this);
// Remove native controls
ui.toggleNativeControls.call(this);
// Setup captions for HTML5
if (this.isHTML5) {
captions.setup.call(this);
}
// Setup captions for HTML5
if (this.isHTML5) {
captions.setup.call(this);
}
// Reset volume
this.volume = null;
// Reset volume
this.volume = null;
// Reset mute state
this.muted = null;
// Reset mute state
this.muted = null;
// Reset speed
this.speed = null;
// Reset loop state
this.loop = null;
// Reset loop state
this.loop = null;
// Reset quality setting
this.quality = null;
// Reset quality setting
this.quality = null;
// Reset speed
this.speed = null;
// Reset volume display
controls.updateVolume.call(this);
// Reset volume display
controls.updateVolume.call(this);
// Reset time display
controls.timeUpdate.call(this);
// Reset time display
controls.timeUpdate.call(this);
// Update the UI
ui.checkPlaying.call(this);
// Update the UI
ui.checkPlaying.call(this);
// Check for picture-in-picture support
toggleClass(
this.elements.container,
this.config.classNames.pip.supported,
support.pip && this.isHTML5 && this.isVideo,
);
// Check for picture-in-picture support
toggleClass(
this.elements.container,
this.config.classNames.pip.supported,
support.pip && this.isHTML5 && this.isVideo,
);
// Check for airplay support
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Check for airplay support
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add iOS class
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
// Add touch class
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
// Ready for API calls
this.ready = true;
// Ready for API calls
this.ready = true;
// Ready event at end of execution stack
setTimeout(() => {
triggerEvent.call(this, this.media, 'ready');
}, 0);
// Ready event at end of execution stack
setTimeout(() => {
triggerEvent.call(this, this.media, 'ready');
}, 0);
// Set the title
ui.setTitle.call(this);
// Set the title
ui.setTitle.call(this);
// Assure the poster image is set, if the property was added before the element was created
if (this.poster) {
ui.setPoster.call(this, this.poster, false).catch(() => {});
}
// Assure the poster image is set, if the property was added before the element was created
if (this.poster) {
ui.setPoster.call(this, this.poster, false).catch(() => {});
}
// Manually set the duration if user has overridden it.
// The event listeners for it doesn't get called if preload is disabled (#701)
if (this.config.duration) {
controls.durationUpdate.call(this);
}
},
// Manually set the duration if user has overridden it.
// The event listeners for it doesn't get called if preload is disabled (#701)
if (this.config.duration) {
controls.durationUpdate.call(this);
}
},
// Setup aria attribute for play and iframe title
setTitle() {
// Find the current text
let label = i18n.get('play', this.config);
// Setup aria attribute for play and iframe title
setTitle() {
// Find the current text
let label = i18n.get('play', this.config);
// If there's a media title set, use that for the label
if (is.string(this.config.title) && !is.empty(this.config.title)) {
label += `, ${this.config.title}`;
}
// If there's a media title set, use that for the label
if (is.string(this.config.title) && !is.empty(this.config.title)) {
label += `, ${this.config.title}`;
}
// If there's a play button, set label
Array.from(this.elements.buttons.play || []).forEach(button => {
button.setAttribute('aria-label', label);
});
// If there's a play button, set label
Array.from(this.elements.buttons.play || []).forEach((button) => {
button.setAttribute('aria-label', label);
});
// Set iframe title
// https://github.com/sampotts/plyr/issues/124
if (this.isEmbed) {
const iframe = getElement.call(this, 'iframe');
// Set iframe title
// https://github.com/sampotts/plyr/issues/124
if (this.isEmbed) {
const iframe = getElement.call(this, 'iframe');
if (!is.element(iframe)) {
return;
}
if (!is.element(iframe)) {
return;
}
// Default to media type
const title = !is.empty(this.config.title) ? this.config.title : 'video';
const format = i18n.get('frameTitle', this.config);
// Default to media type
const title = !is.empty(this.config.title) ? this.config.title : 'video';
const format = i18n.get('frameTitle', this.config);
iframe.setAttribute('title', format.replace('{title}', title));
}
},
iframe.setAttribute('title', format.replace('{title}', title));
}
},
// Toggle poster
togglePoster(enable) {
toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
},
// Toggle poster
togglePoster(enable) {
toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
},
// Set the poster image (async)
// Used internally for the poster setter, with the passive option forced to false
setPoster(poster, passive = true) {
// Don't override if call is passive
if (passive && this.poster) {
return Promise.reject(new Error('Poster already set'));
}
// Set the poster image (async)
// Used internally for the poster setter, with the passive option forced to false
setPoster(poster, passive = true) {
// Don't override if call is passive
if (passive && this.poster) {
return Promise.reject(new Error('Poster already set'));
}
// Set property synchronously to respect the call order
this.media.setAttribute('poster', poster);
// Set property synchronously to respect the call order
this.media.setAttribute('data-poster', poster);
// Wait until ui is ready
return (
ready
.call(this)
// Load image
.then(() => loadImage(poster))
.catch(err => {
// Hide poster on error unless it's been set by another call
if (poster === this.poster) {
ui.togglePoster.call(this, false);
}
// Rethrow
throw err;
})
.then(() => {
// Prevent race conditions
if (poster !== this.poster) {
throw new Error('setPoster cancelled by later call to setPoster');
}
})
.then(() => {
Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`,
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
ui.togglePoster.call(this, true);
return poster;
})
);
},
// Show the poster
this.elements.poster.removeAttribute('hidden');
// Check playing state
checkPlaying(event) {
// Class hooks
toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Wait until ui is ready
return (
ready
.call(this)
// Load image
.then(() => loadImage(poster))
.catch((err) => {
// Hide poster on error unless it's been set by another call
if (poster === this.poster) {
ui.togglePoster.call(this, false);
}
// Rethrow
throw err;
})
.then(() => {
// Prevent race conditions
if (poster !== this.poster) {
throw new Error('setPoster cancelled by later call to setPoster');
}
})
.then(() => {
Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`,
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
// Set state
Array.from(this.elements.buttons.play || []).forEach(target => {
target.pressed = this.playing;
});
ui.togglePoster.call(this, true);
// Only update controls on non timeupdate events
if (is.event(event) && event.type === 'timeupdate') {
return;
}
return poster;
})
);
},
// Toggle controls
// Check playing state
checkPlaying(event) {
// Class hooks
toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set state
Array.from(this.elements.buttons.play || []).forEach((target) => {
Object.assign(target, { pressed: this.playing });
target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
});
// Only update controls on non timeupdate events
if (is.event(event) && event.type === 'timeupdate') {
return;
}
// Toggle controls
ui.toggleControls.call(this);
},
// Check if media is loading
checkLoading(event) {
this.loading = ['stalled', 'waiting'].includes(event.type);
// Clear timer
clearTimeout(this.timers.loading);
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(
() => {
// Update progress bar loading class state
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Update controls visibility
ui.toggleControls.call(this);
},
},
this.loading ? 250 : 0,
);
},
// Check if media is loading
checkLoading(event) {
this.loading = ['stalled', 'waiting'].includes(event.type);
// Toggle controls based on state and `force` argument
toggleControls(force) {
const { controls: controlsElement } = this.elements;
// Clear timer
clearTimeout(this.timers.loading);
if (controlsElement && this.config.hideControls) {
// Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => {
// Update progress bar loading class state
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
this.toggleControls(
Boolean(
force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,
),
);
}
},
// Update controls visibility
ui.toggleControls.call(this);
}, this.loading ? 250 : 0);
},
// Migrate any custom properties from the media to the parent
migrateStyles() {
// Loop through values (as they are the keys when the object is spread 🤔)
Object.values({ ...this.media.style })
// We're only fussed about Plyr specific properties
.filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))
.forEach((key) => {
// Set on the container
this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));
// Toggle controls based on state and `force` argument
toggleControls(force) {
const { controls } = this.elements;
// Clean up from media element
this.media.style.removeProperty(key);
});
if (controls && this.config.hideControls) {
// Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
const recentTouchSeek = (this.touch && this.lastSeekTime + 2000 > Date.now());
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover || recentTouchSeek));
}
},
// Remove attribute if empty
if (is.empty(this.media.style)) {
this.media.removeAttribute('style');
}
},
};
export default ui;

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