Compare commits

...

121 Commits

Author SHA1 Message Date
Sam Potts 333619f1e3 Remove pointer-events: none on embed <iframe> to comply with YouTube ToS 2018-06-21 14:26:40 +10:00
Sam Potts 17dcb63c26 Merge branch 'develop'
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
2018-06-21 09:11:05 +10:00
Sam Potts e04b90c9c0 Ads only on HTML5 and .is cleanup 2018-06-21 09:06:28 +10:00
Sam Potts f62e1da01a Merge pull request #1057 from meyt/patch-1
Fix i18n defaults path on README
2018-06-20 22:46:50 +10:00
Meyti 2fe949629f Fix i18n defaults path 2018-06-20 16:00:51 +04:30
Sam Potts 20f2ddc11d Merge pull request #1056 from friday/volume
Minor increaseVolume and decreaseVolume changes
2018-06-20 16:53:43 +10:00
Albin Larsson 004528a65c Avoid conditions in volume scroll event listener 2018-06-19 17:22:12 +02:00
Albin Larsson 39c7bd40c2 Make decreaseVolume wrap increaseVolume for code reuse 2018-06-19 16:29:52 +02:00
Albin Larsson 43879e08f4 Make (increase/decrease)Volume methods ignore invalid input instead of raising / lowering to the min / max 2018-06-19 16:27:06 +02:00
Sam Potts 5ed7aa6620 v3.3.17 2018-06-19 16:58:38 +10:00
Sam Potts 47750b6aad v3.3.17
-   Fix YouTube muting after seeking with the progress slider
-   Respect preload="none" when setting quality if the media hasn't been loaded some other way
2018-06-19 16:57:32 +10:00
Sam Potts de7832eb8b Merge branch 'develop' 2018-06-19 16:40:54 +10:00
friday 52ea5bd0ab Merge pull request #1052 from friday/youtube-audio-fix
Fix YouTube muting after seeking with the progress slider.
2018-06-19 04:15:07 +02:00
Albin Larsson 457d112df7 Fix #1045: YouTube mutes when seeking after play 2018-06-19 04:03:59 +02:00
Sam Potts 22cdec9d38 Merge pull request #1051 from friday/quality-2
Respect preload="none" when setting quality if the media hasn't been loaded some other way
2018-06-19 11:48:44 +10:00
Albin Larsson d72e502107 Fixes #1044: Don't load the new source if preload is disabled and the current source hasn't been loaded 2018-06-19 03:39:18 +02:00
Albin Larsson 94055f0772 Replace filter()[0] with find() 2018-06-19 03:35:57 +02:00
Sam Potts ede9323524 v3.3.16 2018-06-19 09:16:14 +10:00
Sam Potts c45f428f61 Built files 2018-06-19 09:14:10 +10:00
Sam Potts b61ba02f3d Fix issue with play button not changing state (fixes #1048) 2018-06-19 09:12:21 +10:00
Sam Potts ea4d91d2a0 v3.3.15 2018-06-18 23:21:03 +10:00
Sam Potts 22d524ac9d Removed 1440p so I can afford to eat 2018-06-18 22:16:12 +10:00
Sam Potts 8584f6a1db v3.3.14 2018-06-18 22:01:56 +10:00
Sam Potts 08df96a149 v3.3.13
You guessed it, a load of awesome changes from contributors:

Thanks @friday for the following:

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

-   fix: html5.cancelRequest not remove source tag correctly (thanks @a60814billy)
-   remove event listeners in destroy() (thanks @cky917)
-   Fix markdown in README (thanks @azu)
-   Some parts of the accessibility improvements outlined in #905 (more on the way...)
-   Fix for bug where volume slider didn't always show
2018-06-18 21:49:06 +10:00
Sam Potts cc3c0b5448 Merge branch 'develop' 2018-06-18 21:41:25 +10:00
Sam Potts 3c9c1b4cdc Merge pull request #1041 from sampotts/a11y-improvements
A11y improvements
2018-06-17 01:34:11 +10:00
Sam Potts 599883e684 Formatting fix 2018-06-17 01:30:24 +10:00
Sam Potts f1b4db4f36 Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	src/js/controls.js
#	src/js/fullscreen.js
#	src/js/plyr.js
#	src/js/ui.js
#	src/js/utils.js
2018-06-17 01:26:24 +10:00
Sam Potts d4abb4b143 120 line width, package upgrade 2018-06-17 01:04:55 +10:00
Sam Potts 828ce66942 Merge pull request #1038 from friday/captions-input-lowercase
Small captions fixes
2018-06-17 00:40:56 +10:00
Sam Potts ccc2608cf6 Merge pull request #1039 from friday/poster-race-conditions
Fix poster race conditions
2018-06-17 00:40:28 +10:00
Sam Potts de45de0e0b Merge pull request #1040 from friday/switches-get-stitches
Switches code optimizations
2018-06-17 00:39:35 +10:00
Albin Larsson 99c10aa1fc Replace switch in controls.createProgress with object literal 2018-06-16 07:27:04 +02:00
Albin Larsson 2a186e425b Replace switch in support.mime with object literal and conditions, and make it return boolean 2018-06-16 07:27:04 +02:00
Albin Larsson 64bb206d85 Replace switch in support.check with simpler conditions 2018-06-16 07:27:04 +02:00
Albin Larsson 2d6732d580 Replace switch in controls.createLabel with object literal 2018-06-16 07:25:34 +02:00
Albin Larsson 8f359adf9c Fix captions.toggle order 2018-06-16 01:34:55 +02:00
Albin Larsson 1f09493ba2 Captions: Handle uppercase input (like before) 2018-06-16 01:07:16 +02:00
Albin Larsson 115f352ade Respect call order and prioritize public API calls for setting poster, in order to avoid race conditions 2018-06-15 23:56:47 +02:00
Albin Larsson 2af60c5c0d Add 'ready' promise 2018-06-15 23:01:33 +02:00
Albin Larsson aab2817ddc Copy poster when creating new media element for YouTube and Vimeo (needed for #1018) 2018-06-15 22:57:16 +02:00
Albin Larsson f1c4752036 Filter out null / undefined in elements.setAttributes 2018-06-15 22:52:19 +02:00
Albin Larsson 88735e3146 Replace switch in controls.updateSetting with condition 2018-06-15 15:57:10 +02:00
Albin Larsson c373ed72d7 Replace switch in YouTube error handler with object literal 2018-06-15 15:57:10 +02:00
Albin Larsson 213cfe8c84 Replace switch in media.js with simpler conditions 2018-06-15 15:57:10 +02:00
Albin Larsson 87ea5e14b4 Replace provider switch plyr.js with conditions 2018-06-15 15:57:10 +02:00
Albin Larsson 2aa967aba9 Replace switch in source.js with destructuring 2018-06-15 12:33:30 +02:00
Sam Potts d522e40594 Merge pull request #1034 from friday/remove-array-newline-rule
Suggestion: Remove array newline rule
2018-06-15 15:34:29 +10:00
Sam Potts 3cd2b9a6c3 Merge pull request #1036 from friday/captions-passive-toggle
Captions fixes (again)
2018-06-15 15:33:39 +10:00
Albin Larsson 19e412a73a Add 'passive' flag to internal captions methods to avoid overriding user preferences, support multiple browser languages (get first match) and improve comments 2018-06-15 06:07:04 +02:00
Albin Larsson cf5f77c709 Fix menu transitionend event listener 2018-06-15 05:51:23 +02:00
Sam Potts 4811e3333f Merge pull request #1035 from sampotts/friday-contrib-2
Contributions improvements
2018-06-15 11:04:33 +10:00
Albin Larsson 8257857075 Wrap caption toggle event listener callback to avoid sending event 2018-06-15 02:44:00 +02:00
friday e3e4e60fdb Contributions improvements
General improvements and new sections
2018-06-14 19:50:59 +02:00
Albin Larsson 6ce9a94932 Move internal event listeners for captions with direct handling in the captions object 2018-06-14 16:41:16 +02:00
Albin Larsson fa5d0ad316 Move toggleCaption internals to captions object 2018-06-14 15:58:35 +02:00
Albin Larsson 6bff6b317d Remove line breaks in arrays 2018-06-13 23:27:35 +02:00
Albin Larsson 99ac8d4c52 Remove array-newline rule 2018-06-13 22:07:32 +02:00
Sam Potts 019e1f80ca Merge pull request #1032 from friday/event-2
Fix condition in events.toggleListener to allow non-elements
2018-06-13 23:16:46 +10:00
Albin Larsson 2fe98f3721 Fix condition in events.toggleListener to allow non-elements 2018-06-13 14:29:55 +02:00
Sam Potts 5c08363400 Merge pull request #1030 from friday/event-improvements
Minor event changes
2018-06-13 10:52:17 +10:00
Albin Larsson 927326f715 Also remove 'once' event listeners when destroying (they may still be waiting) 2018-06-12 20:00:41 +02:00
Albin Larsson 53933dff7e Use toggleListener in trapFocus 2018-06-12 19:39:26 +02:00
Albin Larsson f15c1344b0 Removed support for multiple elements in toggleListener 2018-06-12 19:10:00 +02:00
Albin Larsson fb48b330cc typo 2018-06-12 17:41:17 +02:00
Sam Potts 5dddf8b0ec Logic cleanup 2018-06-13 00:56:31 +10:00
Sam Potts 0ecf7e3854 Force string on format 2018-06-13 00:48:42 +10:00
Sam Potts aae1092bac Merge branch 'develop' of github.com:sampotts/plyr into develop
# Conflicts:
#	src/js/captions.js
#	src/js/controls.js
#	src/js/fullscreen.js
#	src/js/html5.js
#	src/js/listeners.js
#	src/js/plugins/youtube.js
#	src/js/plyr.js
#	src/js/utils.js
2018-06-13 00:41:30 +10:00
Sam Potts 7158e507ad Merge pull request #1029 from cky917/develop
remove event listeners in destroy()
2018-06-13 00:05:31 +10:00
Sam Potts 70f3390ffe Merge pull request #1028 from a60814billy/fix/cancel-request-not-remove-source-tag-correctly
fix: html5.cancelRequest not remove source tag correctly
2018-06-13 00:03:24 +10:00
Sam Potts 392dfd024c Utils broken down into seperate files and exports 2018-06-13 00:02:55 +10:00
cky 87170ab460 remove event listeners in destroy, add once method 2018-06-12 21:18:05 +08:00
BoHong Li ee4c044d27 fix: html5.cancelRequest not remove source tag correctly 2018-06-12 11:35:31 +08:00
Sam Potts 0b09b8ee6f Merge pull request #1027 from friday/quality
Minor code improvements for quality switching
2018-06-12 11:13:34 +10:00
Albin Larsson db95b3234f Move uniqueness filter from getQualityOptions to setQualityMenu 2018-06-12 02:31:18 +02:00
Albin Larsson 6d2dad5810 Trigger qualityrequested event unconditionally when trying to set it (needed for streaming libraries to be able to listen) 2018-06-12 02:31:18 +02:00
Albin Larsson 81ee3f759c Remove todo comment about Vimeo support for setting quality (they don't support it) 2018-06-12 02:31:18 +02:00
Albin Larsson ed606c28ab Filter out unsupported mimetypes in getSources() instead of the quality setter 2018-06-12 02:31:18 +02:00
Albin Larsson f15e07f7f5 Simplify logic in youtube.mapQualityUnit (not that it matters much now) 2018-06-12 02:31:04 +02:00
Sam Potts cd14c3086d Merge pull request #1025 from azu/patch-1
Fix markdown in README
2018-06-11 20:16:18 +10:00
azu 41184b82ee Fix markdown in README 2018-06-11 19:12:35 +09:00
Sam Potts 6a6f3914c0 Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
2018-06-11 17:13:09 +10:00
Sam Potts 840e31a693 v3.3.12 2018-06-11 17:10:37 +10:00
Sam Potts 1bc452c349 Merge 2018-06-11 16:54:35 +10:00
Sam Potts 3fad6ed42c Merge branch 'develop' into a11y-improvements
# Conflicts:
#	demo/dist/demo.css
#	dist/plyr.css
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	src/js/captions.js
2018-06-11 16:54:20 +10:00
Sam Potts 38f954ef17 Merge branch 'master' into develop
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
2018-06-11 16:48:54 +10:00
Sam Potts abd1182303 Merge 2018-06-11 16:45:40 +10:00
Sam Potts efe70ab48e v3.3.11 2018-06-11 16:39:35 +10:00
Albin Larsson 62c263bda3 Replace quality setter conditions with Array.find() 2018-06-11 08:23:08 +02:00
Albin Larsson 4c1337b4c5 Assure type safety in getSources() and getQualityOptions() (always return arrays), and remove external conditions and type conversion no longer needed 2018-06-11 08:23:08 +02:00
Sam Potts 38f10d4cc6 WIP 2018-06-11 16:19:11 +10:00
Sam Potts 1ad76800b0 Merge pull request #1024 from friday/event-bubble-detail
Event "detail" is lost in the synthetic event bubble/proxy
2018-06-11 16:13:02 +10:00
Albin Larsson cc97d7be6a Fix synthetic event bubble/proxy loses detail 2018-06-11 08:00:46 +02:00
Sam Potts f951cb372c Merge pull request #1023 from friday/make-utils-static
Make utils static
2018-06-11 14:41:06 +10:00
Albin Larsson 37a3ab202a Remove wrapper function around utils.is.element in Plyr.setup() (no lnger needed) 2018-06-11 05:44:57 +02:00
Albin Larsson b148adc0af Avoid using this to refer to utils or utils.is, since that means methods can't be used statically 2018-06-11 05:44:57 +02:00
Albin Larsson 16828e975a Move utils.is.getConstructor() to utils.getConstructor() 2018-06-11 05:44:57 +02:00
Sam Potts 7d26f41d64 Merge pull request #1015 from friday/captions-fixes-again
Captions rewrite (use index internally to support missing or duplicate languages)
2018-06-11 13:21:05 +10:00
Sam Potts f37f465ce4 Merge pull request #1020 from friday/1016
Vimeo: Update playback state and assure events are triggered on load
2018-06-11 11:48:03 +10:00
Sam Potts b199215525 Merge pull request #1021 from friday/vimeo-seek-while-playing
Fix for YouTube and Vimeo pausing after seek
2018-06-11 11:47:34 +10:00
Albin Larsson 94699f3255 Fix problem with YouTube and Vimeo seeking while playing 2018-06-11 02:21:00 +02:00
Albin Larsson d3e98eb27e Vimeo: Assure state is updated with autoplay (fixes #1016) 2018-06-11 00:00:16 +02:00
Albin Larsson 41012a9843 Typo 2018-06-10 22:00:15 +02:00
Albin Larsson c83487a293 Fix #1017, fix #980, fix #1014: Captions rewrite (use index internally) 2018-06-10 19:00:07 +02:00
Albin Larsson 1fab4919c0 controls.createMenuItem: Change input to object (too many params made it hard to read) 2018-06-10 18:57:19 +02:00
Albin Larsson 9dc0f28800 Avoid condition in getTracks 2018-06-10 18:57:19 +02:00
Albin Larsson b57784d1a5 Change debug warn 'Unsupported language option' to log 'Language option doesn't yet exist' since it doesn't have to be an error 2018-06-10 18:56:13 +02:00
Albin Larsson a80b31bf98 Fix #1003: Formatted captions issue 2018-06-10 18:56:13 +02:00
Sam Potts 7c6d4666e9 Merge branch 'develop' into a11y-improvements
# Conflicts:
#	demo/dist/demo.css
#	dist/plyr.css
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	src/js/captions.js
#	src/js/plyr.js
2018-06-09 17:03:16 +10:00
Sam Potts 76bb299c68 Restore default 2018-06-09 12:05:37 +10:00
Sam Potts 0c03accd41 Fix Sprite issue 2018-06-09 12:04:53 +10:00
Albin Larsson b12eeb0eb7 Merge captions setText and setCue into updateCues (fixes #998 and vimeo cuechange event) 2018-06-08 11:44:15 +02:00
Sam Potts 8e634862ff Merge pull request #1007 from cky917/master
fix:  After clicking on the progress bar, keyboard operations will not work.
2018-06-08 10:54:16 +10:00
Sam Potts 1e1874d86b Merge pull request #1009 from friday/contributing
Contributing document and codepen demo updates
2018-06-08 10:52:37 +10:00
Albin Larsson 16624b90d3 Clarifications due to recent non-constructive comments in #1001 2018-06-07 14:30:05 +02:00
Albin Larsson ed14b656a8 Add contributing document 2018-06-07 11:47:51 +02:00
Albin Larsson 1d0cf16254 Readme: Replace streaming section with codepen templates for all supported formats and libraries (and updated code) 2018-06-07 11:47:34 +02:00
cky 84424f7f67 fix: when the seek input is focused and the video is playing, the space key can't make the video pause, because after 'keyup', it always make the video play 2018-06-06 19:27:07 +08:00
cky c95d9923f7 fix: https://github.com/sampotts/plyr/issues/1006 2018-06-06 16:59:42 +08:00
Sam Potts 05b85da3f4 Update controls.md (fixes #996) 2018-06-02 19:48:48 +10:00
Sam Potts 90c5735904 WIP 2018-05-28 10:19:07 +10:00
61 changed files with 28058 additions and 27817 deletions
+1 -3
View File
@@ -32,9 +32,7 @@
"message": "Use local parameter instead." "message": "Use local parameter instead."
} }
], ],
"no-param-reassign": [2, { "props": false }], "no-param-reassign": [2, { "props": false }]
"array-bracket-newline": [2, { "minItems": 2 }],
"array-element-newline": [2, { "minItems": 2 }]
}, },
"parserOptions": { "parserOptions": {
"sourceType": "module" "sourceType": "module"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"useTabs": false, "useTabs": false,
"tabWidth": 4, "tabWidth": 4,
"printWidth": 160, "printWidth": 120,
"singleQuote": true, "singleQuote": true,
"trailingComma": "all" "trailingComma": "all"
} }
+61
View File
@@ -0,0 +1,61 @@
# Contributing
We welcome bug reports, feature requests and pull requests. If you want to help us out, please follow these guidelines, in order to avoid redundant work.
## Commenting
When commenting, keep a civil tone and stay on topic. Don't ask for support (use [Stack Overflow](https://stackoverflow.com/) or [our Slack](https://bit.ly/plyr-chat) for that), 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.
## Reporting issues
Our GitHub issue tracker is for bug reports and feature requests. Don't ask for support here. Use [Stack Overflow](https://stackoverflow.com/) or [our Slack](https://bit.ly/plyr-chat) for that.
Please verify that your issue hasn't already been answered by our [FAQ](https://github.com/sampotts/plyr/wiki/FAQ), and that there isn't already an open issue for it.
When applicable, check that your problem doesn't happen without Plyr (see [FAQ#1](https://github.com/sampotts/plyr/wiki/FAQ#1-does-plyr-work-with--)).
Verify that you are following the documentation, are using the latest version of Plyr, and aren't getting any errors in your own code, causing the issues.
Create one issue per problem or request (i.e. don't combine multiple problems to one git issue). Describe the issue as detailed as possible (see [Replication](#replication))
## Replication
In order to solve a problem, we first need to understand it. Please answer these questions when reporting issues or asking for help in [our Slack](https://bit.ly/plyr-chat).
* Does it happen only with specific options and/or specific browsers?
* Does is happen only with HTML5 video, audio, YouTube, Vimeo or a specific library?
* Does the issue happen on [our demo](https://plyr.io/)? If not, please recreate it with a **minimal** example online. You can use our Codepen templates to get started:
* [HTML5 video](https://codepen.io/pen?template=bKeqpr)
* [HTML5 audio](https://codepen.io/pen?template=rKLywR)
* [YouTube](https://codepen.io/pen?template=GGqbbJ)
* [Vimeo](https://codepen.io/pen?template=bKeXNq)
* [Dash.js integration](https://codepen.io/pen?template=zaBgBy)
* [Hls.js integration](https://codepen.io/pen?template=oyLKQb)
* [Shaka Player integration](https://codepen.io/pen?template=ZRpzZO)
It's important that you keep the issue description and replication demo **minimal**. If your replication includes frameworks, libraries or customizations, this makes it harder to debug and understand the issue. While it may be relevant to bring this up (ex: "I need Plyr to trigger the event sooner or it breaks Framework X"), please keep these out of your replication demo if they aren't strictly needed to reproduce the issue. If the issue is caused by something a library does that Plyr doesn't handle, it's more helpful for us if you find out what it is, and replicate the same problem without the library. Otherwise any developer who is willing to help out with the issue has to understand the frameworks, libraries and customizations of *your* choice, or no one will try to fix your issue because it's too much work.
## Requesting features and improvements
If you are missing something in Plyr, you can create a GitHub issue for this as well. Since we prioritize fixing bugs first, and may have a lot of other suggestions and architectural changes to work on as well, these may not be at the top of our list.
If your suggestion is important or urgent to you, you may want to first ensure it's something we want to have in Plyr, and then contribute it as a pull request. [Our Slack](https://bit.ly/plyr-chat) is the best place for questions like this.
## Contributing features and documentation
* Fork Plyr, and create a new branch in your fork, based on the **develop** branch
* To test locally, you can use the demo. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build while you are working, and run a local server from the repository root directory. If you have Python installed, this command should work: `python -m SimpleHTTPServer 8080`. Then go to `http://localhost:8080/demo/`
* Develop and test your modifications.
* Preferably commit your changes as independent logical chunks, with meaningful messages. Make sure you do not commit unnecessary files or changes, such as the build output, or logging and breakpoints you added for testing.
* If your modifications changes the documented behavior or add new features, document these changes in readme.md.
* When finished, push the changes to your GitHub repository and send a pull request to **develop**. Describe what your PR does.
* If the Travis build fails, or if you get a code review with change requests, you can fix these by pushing new or rebased commits to the branch.
+460 -403
View File
File diff suppressed because it is too large Load Diff
+15 -17
View File
@@ -2,9 +2,9 @@
This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs. You can pass the following to the `controls` option: This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs. You can pass the following to the `controls` option:
* `Array` of options (this builds the default controls based on your choices) - `Array` of options (this builds the default controls based on your choices)
* `String` containing the desired HTML - `String` containing the desired HTML
* `Function` that will be executed and should return one of the above - `Function` that will be executed and should return one of the above
## Using default controls ## Using default controls
@@ -81,14 +81,14 @@ The classes and data attributes used in your template should match the `selector
You need to add several placeholders to your HTML template that are replaced when rendering: You need to add several placeholders to your HTML template that are replaced when rendering:
* `{id}` - the dynamically generated ID for the player (for form controls) - `{id}` - the dynamically generated ID for the player (for form controls)
* `{seektime}` - the seek time specified in options for fast forward and rewind - `{seektime}` - the seek time specified in options for fast forward and rewind
* `{title}` - the title of your media, if specified - `{title}` - the title of your media, if specified
### Limitations ### Limitations
* Currently the settings menus are not supported with custom controls HTML - Currently the settings menus are not supported with custom controls HTML
* AirPlay and PiP buttons can be added but you will have to manage feature detection - AirPlay and PiP buttons can be added but you will have to manage feature detection
### Example ### Example
@@ -105,7 +105,7 @@ const controls = `
<svg role="presentation"><use xlink:href="#plyr-rewind"></use></svg> <svg role="presentation"><use xlink:href="#plyr-rewind"></use></svg>
<span class="plyr__tooltip" role="tooltip">Rewind {seektime} secs</span> <span class="plyr__tooltip" role="tooltip">Rewind {seektime} secs</span>
</button> </button>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Play, {title}" data-plyr="play"> <button type="button" class="plyr__control" aria-label="Play, {title}" data-plyr="play">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-pause"></use></svg> <svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-pause"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-play"></use></svg> <svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-play"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Pause</span> <span class="label--pressed plyr__tooltip" role="tooltip">Pause</span>
@@ -116,30 +116,28 @@ const controls = `
<span class="plyr__tooltip" role="tooltip">Forward {seektime} secs</span> <span class="plyr__tooltip" role="tooltip">Forward {seektime} secs</span>
</button> </button>
<div class="plyr__progress"> <div class="plyr__progress">
<label for="plyr-seek-{id}" class="plyr__sr-only">Seek</label> <input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" aria-label="Seek">
<input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" id="plyr-seek-{id}"> <progress class="plyr__progress__buffer" min="0" max="100" value="0">% buffered</progress>
<progress class="plyr__progress--buffer" min="0" max="100" value="0">% buffered</progress>
<span role="tooltip" class="plyr__tooltip">00:00</span> <span role="tooltip" class="plyr__tooltip">00:00</span>
</div> </div>
<div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div> <div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
<div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div> <div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute"> <button type="button" class="plyr__control" aria-label="Mute" data-plyr="mute">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg> <svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg> <svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Unmute</span> <span class="label--pressed plyr__tooltip" role="tooltip">Unmute</span>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Mute</span> <span class="label--not-pressed plyr__tooltip" role="tooltip">Mute</span>
</button> </button>
<div class="plyr__volume"> <div class="plyr__volume">
<label for="plyr-volume-{id}" class="plyr__sr-only">Volume</label> <input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" aria-label="Volume">
<input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" id="plyr-volume-{id}">
</div> </div>
<button type="button" class="plyr__control" aria-pressed="true" aria-label="Enable captions" data-plyr="captions"> <button type="button" class="plyr__control" data-plyr="captions">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg> <svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-captions-off"></use></svg> <svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-captions-off"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Disable captions</span> <span class="label--pressed plyr__tooltip" role="tooltip">Disable captions</span>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enable captions</span> <span class="label--not-pressed plyr__tooltip" role="tooltip">Enable captions</span>
</button> </button>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Enter fullscreen" data-plyr="fullscreen"> <button type="button" class="plyr__control" data-plyr="fullscreen">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-exit-fullscreen"></use></svg> <svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-exit-fullscreen"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-enter-fullscreen"></use></svg> <svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-enter-fullscreen"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Exit fullscreen</span> <span class="label--pressed plyr__tooltip" role="tooltip">Exit fullscreen</span>
+1 -1
View File
File diff suppressed because one or more lines are too long
+4391 -4287
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -96,7 +96,7 @@
<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-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-720p.mp4" type="video/mp4" size="720">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080"> <source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4" type="video/mp4" size="1440"> <!-- <source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4" type="video/mp4" size="1440"> -->
<!-- Caption files --> <!-- 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" <track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
+17 -9
View File
@@ -143,7 +143,11 @@ import Raven from 'raven-js';
// Set a new source // Set a new source
function newSource(type, init) { 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 // Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
if (!(type in types) || (!init && type === currentType) || (!currentType.length && type === types.video)) { if (
!(type in types) ||
(!init && type === currentType) ||
(!currentType.length && type === types.video)
) {
return; return;
} }
@@ -215,10 +219,12 @@ import Raven from 'raven-js';
case types.youtube: case types.youtube:
player.source = { player.source = {
type: 'video', type: 'video',
sources: [{ sources: [
src: 'https://youtube.com/watch?v=bTqVqk7FSmY', {
provider: 'youtube', src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
}], provider: 'youtube',
},
],
}; };
break; break;
@@ -226,10 +232,12 @@ import Raven from 'raven-js';
case types.vimeo: case types.vimeo:
player.source = { player.source = {
type: 'video', type: 'video',
sources: [{ sources: [
src: 'https://vimeo.com/76979871', {
provider: 'vimeo', src: 'https://vimeo.com/76979871',
}], provider: 'vimeo',
},
],
}; };
break; break;
+1 -1
View File
File diff suppressed because one or more lines are too long
+7489 -7475
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+13329 -13318
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+7 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "plyr", "name": "plyr",
"version": "3.3.10", "version": "3.3.19",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player", "description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io", "homepage": "https://plyr.io",
"main": "./dist/plyr.js", "main": "./dist/plyr.js",
@@ -20,14 +20,14 @@
"git-branch": "^2.0.1", "git-branch": "^2.0.1",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-autoprefixer": "^5.0.0", "gulp-autoprefixer": "^5.0.0",
"gulp-better-rollup": "^3.1.0", "gulp-better-rollup": "^3.2.1",
"gulp-clean-css": "^3.9.4", "gulp-clean-css": "^3.9.4",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-filter": "^5.1.0", "gulp-filter": "^5.1.0",
"gulp-header": "^2.0.5", "gulp-header": "^2.0.5",
"gulp-open": "^3.0.1", "gulp-open": "^3.0.1",
"gulp-postcss": "^7.0.1", "gulp-postcss": "^7.0.1",
"gulp-rename": "^1.2.3", "gulp-rename": "^1.3.0",
"gulp-replace": "^1.0.0", "gulp-replace": "^1.0.0",
"gulp-s3": "^0.11.0", "gulp-s3": "^0.11.0",
"gulp-sass": "^4.0.1", "gulp-sass": "^4.0.1",
@@ -44,12 +44,12 @@
"rollup-plugin-commonjs": "^9.1.3", "rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-node-resolve": "^3.3.0", "rollup-plugin-node-resolve": "^3.3.0",
"run-sequence": "^2.2.1", "run-sequence": "^2.2.1",
"stylelint": "^9.2.1", "stylelint": "^9.3.0",
"stylelint-config-prettier": "^3.2.0", "stylelint-config-prettier": "^3.3.0",
"stylelint-config-recommended": "^2.1.0", "stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.0.0", "stylelint-config-sass-guidelines": "^5.0.0",
"stylelint-order": "^0.8.1", "stylelint-order": "^0.8.1",
"stylelint-scss": "^3.1.0", "stylelint-scss": "^3.1.3",
"stylelint-selector-bem-pattern": "^2.0.0" "stylelint-selector-bem-pattern": "^2.0.0"
}, },
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"], "keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
@@ -74,7 +74,7 @@
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"custom-event-polyfill": "^0.3.0", "custom-event-polyfill": "^0.3.0",
"loadjs": "^3.5.4", "loadjs": "^3.5.4",
"raven-js": "^3.25.2", "raven-js": "^3.26.3",
"url-polyfill": "^1.0.13" "url-polyfill": "^1.0.13"
} }
} }
+2 -1
View File
@@ -11,7 +11,8 @@
}, },
// Exclude from search // Exclude from search
"search.exclude": { "search.exclude": {
"dist/": true "dist/": true,
"demo/dist/": true
}, },
// Linting // Linting
"stylelint.enable": true, "stylelint.enable": true,
+13 -18
View File
@@ -51,6 +51,10 @@ Some awesome folks have made plugins for CMSs and Components for JavaScript fram
Here's a quick run through on getting up and running. There's also a [demo on Codepen](http://codepen.io/sampotts/pen/jARJYp). You can grab all of the source with [NPM](https://www.npmjs.com/package/plyr) using `npm install plyr`. Here's a quick run through on getting up and running. There's also a [demo on Codepen](http://codepen.io/sampotts/pen/jARJYp). You can grab all of the source with [NPM](https://www.npmjs.com/package/plyr) using `npm install plyr`.
### Try Plyr online
You can try Plyr in Codepen using our minimal templates: [HTML5 video](https://codepen.io/pen?template=bKeqpr), [HTML5 audio](https://codepen.io/pen?template=rKLywR), [YouTube](https://codepen.io/pen?template=GGqbbJ), [Vimeo](https://codepen.io/pen?template=bKeXNq). For Streaming we also have example integrations with: [Dash.js](https://codepen.io/pen?template=zaBgBy), [Hls.js](https://codepen.io/pen?template=oyLKQb) and [Shaka Player](https://codepen.io/pen?template=ZRpzZO)
### HTML ### HTML
Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement) markup so that's all you need for those types. Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement) markup so that's all you need for those types.
@@ -128,13 +132,13 @@ See [initialising](#initialising) for more information on advanced setups.
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build. You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
```html ```html
<script src="https://cdn.plyr.io/3.3.10/plyr.js"></script> <script src="https://cdn.plyr.io/3.3.19/plyr.js"></script>
``` ```
...or... ...or...
```html ```html
<script src="https://cdn.plyr.io/3.3.10/plyr.polyfilled.js"></script> <script src="https://cdn.plyr.io/3.3.19/plyr.polyfilled.js"></script>
``` ```
### CSS ### CSS
@@ -148,13 +152,13 @@ Include the `plyr.css` stylsheet into your `<head>`
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following: If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
```html ```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.10/plyr.css"> <link rel="stylesheet" href="https://cdn.plyr.io/3.3.19/plyr.css">
``` ```
### SVG Sprite ### SVG Sprite
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.10/plyr.svg`. reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.19/plyr.svg`.
## Ads ## Ads
@@ -211,7 +215,7 @@ You can specify a range of arguments for the constructor to use:
* A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) * A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
* A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) * A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
* A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList) * A [`NodeList`](https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
* A [jQuery](https://jquery.com) object * A [jQuery](https://jquery.com) object
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below. _Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below.
@@ -282,7 +286,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `debug` | Boolean | `false` | Display debugging information in the console | | `debug` | Boolean | `false` | Display debugging information in the console |
| `controls` | Array, Function or Element | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function; `id` (the unique id for the player), `seektime` (the seektime step in seconds), and `title` (the media title). See [controls.md](controls.md) for more info on how the html needs to be structured. | | `controls` | Array, Function or Element | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function; `id` (the unique id for the player), `seektime` (the seektime step in seconds), and `title` (the media title). See [controls.md](controls.md) for more info on how the html needs to be structured. |
| `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If you're using the default controls are used then you can specify which settings to show in the menu | | `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If you're using the default controls are used then you can specify which settings to show in the menu |
| `i18n` | Object | See [defaults.js](/src/js/defaults.js) | Used for internationalization (i18n) of the text within the UI. | | `i18n` | Object | See [defaults.js](/src/js/config/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
| `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. | | `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. |
| `iconUrl` | String | `null` | Specify a URL or path to the SVG sprite. See the [SVG section](#svg) for more info. | | `iconUrl` | String | `null` | Specify a URL or path to the SVG sprite. See the [SVG section](#svg) for more info. |
| `iconPrefix` | String | `plyr` | Specify the id prefix for the icons used in the default controls (e.g. "plyr-play" would be "plyr"). This is to prevent clashes if you're using your own SVG sprite but with the default controls. Most people can ignore this option. | | `iconPrefix` | String | `plyr` | Specify the id prefix for the icons used in the default controls (e.g. "plyr-play" would be "plyr"). This is to prevent clashes if you're using your own SVG sprite but with the default controls. Most people can ignore this option. |
@@ -363,6 +367,7 @@ player.fullscreen.enter(); // Enter fullscreen
| `airplay()` | - | Trigger the airplay dialog on supported devices. | | `airplay()` | - | Trigger the airplay dialog on supported devices. |
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. | | `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
| `on(event, function)` | String, Function | Add an event listener for the specified event. | | `on(event, function)` | String, Function | Add an event listener for the specified event. |
| `once(event, function)` | String, Function | Add an event listener for the specified event once. |
| `off(event, function)` | String, Function | Remove an event listener for the specified event. | | `off(event, function)` | String, Function | Remove an event listener for the specified event. |
| `supports(type)` | String | Check support for a mime type. | | `supports(type)` | String | Check support for a mime type. |
| `destroy()` | - | Destroy the instance and garbage collect any elements. | | `destroy()` | - | Destroy the instance and garbage collect any elements. |
@@ -407,7 +412,8 @@ player.fullscreen.active; // false;
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. | | `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
| `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. | | `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. | | `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. | | `currentTrack` | ✓ | ✓ | Gets or sets the caption track by index. `-1` means the track is missing or captions is not active |
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. If your captions don't have any language data, or if you have multiple tracks with the same language, you may want to use `currentTrack` instead. |
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. | | `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. | | `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
| `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. | | `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. |
@@ -607,17 +613,6 @@ document then the shortcuts will work when any element has focus, apart from an
| `C` | Toggle captions | | `C` | Toggle captions |
| `L` | Toggle loop | | `L` | Toggle loop |
## Streaming
Because Plyr is an extension of the standard HTML5 video and audio elements, third party streaming plugins can be used with Plyr. Massive thanks to Matias
Russitto ([@russitto](https://github.com/russitto)) for working on this. Here's a few examples:
* Using [hls.js](https://github.com/dailymotion/hls.js) - [Demo](http://codepen.io/sampotts/pen/JKEMqB)
* Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR)
* Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN)
_Note_: These need updating to use the new v3 syntax but would still work.
## Fullscreen ## Fullscreen
Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen). Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen).
+267 -133
View File
@@ -6,7 +6,21 @@
import controls from './controls'; import controls from './controls';
import i18n from './i18n'; import i18n from './i18n';
import support from './support'; import support from './support';
import utils from './utils'; import { dedupe } from './utils/arrays';
import browser from './utils/browser';
import {
createElement,
emptyElement,
getAttributesFromSelector,
insertAfter,
removeElement,
toggleClass,
} from './utils/elements';
import { on, triggerEvent } from './utils/events';
import fetch from './utils/fetch';
import is from './utils/is';
import { getHTML } from './utils/strings';
import { parseUrl } from './utils/urls';
const captions = { const captions = {
// Setup captions // Setup captions
@@ -19,7 +33,11 @@ const captions = {
// Only Vimeo and HTML5 video supported at this point // Only Vimeo and HTML5 video supported at this point
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) { if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
// Clear menu and hide // Clear menu and hide
if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { if (
is.array(this.config.controls) &&
this.config.controls.includes('settings') &&
this.config.settings.includes('captions')
) {
controls.setCaptionsMenu.call(this); controls.setCaptionsMenu.call(this);
} }
@@ -27,15 +45,12 @@ const captions = {
} }
// Inject the container // Inject the container
if (!utils.is.element(this.elements.captions)) { if (!is.element(this.elements.captions)) {
this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions)); this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
utils.insertAfter(this.elements.captions, this.elements.wrapper); insertAfter(this.elements.captions, this.elements.wrapper);
} }
// Get browser info
const browser = utils.getBrowser();
// Fix IE captions if CORS is used // Fix IE captions if CORS is used
// Fetch captions and inject as blobs instead (data URIs not supported!) // Fetch captions and inject as blobs instead (data URIs not supported!)
if (browser.isIE && window.URL) { if (browser.isIE && window.URL) {
@@ -43,60 +58,96 @@ const captions = {
Array.from(elements).forEach(track => { Array.from(elements).forEach(track => {
const src = track.getAttribute('src'); const src = track.getAttribute('src');
const href = utils.parseUrl(src); const url = parseUrl(src);
if (href.hostname !== window.location.href.hostname && [ if (
'http:', url !== null &&
'https:', url.hostname !== window.location.href.hostname &&
].includes(href.protocol)) { ['http:', 'https:'].includes(url.protocol)
utils ) {
.fetch(src, 'blob') fetch(src, 'blob')
.then(blob => { .then(blob => {
track.setAttribute('src', window.URL.createObjectURL(blob)); track.setAttribute('src', window.URL.createObjectURL(blob));
}) })
.catch(() => { .catch(() => {
utils.removeElement(track); removeElement(track);
}); });
} }
}); });
} }
// Try to load the value from storage // Get and set initial data
let active = this.storage.get('captions'); // The "preferred" options are not realized unless / until the wanted language has a match
// * languages: Array of user's browser languages.
// * language: The language preferred by user settings or config
// * active: The state preferred by user settings or config
// * toggled: The real captions state
// Otherwise fall back to the default config const languages = dedupe(
if (!utils.is.boolean(active)) { Array.from(navigator.languages || navigator.userLanguage).map(language => language.split('-')[0]),
);
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
// Use first browser language when language is 'auto'
if (language === 'auto') {
[language] = languages;
}
let active = this.storage.get('captions');
if (!is.boolean(active)) {
({ active } = this.config.captions); ({ active } = this.config.captions);
} }
// Set toggled state Object.assign(this.captions, {
this.toggleCaptions(active); toggled: false,
active,
language,
languages,
});
// Watch changes to textTracks and update captions menu // Watch changes to textTracks and update captions menu
if (this.config.captions.update) { if (this.isHTML5) {
utils.on(this.media.textTracks, 'addtrack removetrack', captions.update.bind(this)); 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) // Update available languages in list next tick (the event must not be triggered before the listeners)
setTimeout(captions.update.bind(this), 0); setTimeout(captions.update.bind(this), 0);
}, },
// Update available language options in settings based on tracks
update() { update() {
// Update tracks const tracks = captions.getTracks.call(this, true);
const tracks = captions.getTracks.call(this); // Get the wanted language
this.options.captions = tracks.map(({language}) => language); const { active, language, meta, currentTrackNode } = this.captions;
const languageExists = Boolean(tracks.find(track => track.language === language));
// Set language if it hasn't been set already // Handle tracks (add event listener and "pseudo"-default)
if (!this.language) { if (this.isHTML5 && this.isVideo) {
let { language } = this.config.captions; tracks.filter(track => !meta.get(track)).forEach(track => {
if (language === 'auto') { this.debug.log('Track added', track);
[ language ] = (navigator.language || navigator.userLanguage).split('-'); // Attempt to store if the original dom element was "default"
} meta.set(track, {
this.language = this.storage.get('language') || (language || '').toLowerCase(); default: track.mode === 'showing',
});
// Turn off native caption rendering to avoid double captions
track.mode = 'hidden';
// Add event listener for cue changes
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
});
} }
// Toggle the class hooks // Update language first time it matches, or if the previous matching track was removed
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this))); 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 // Update available languages in list
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) { if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
@@ -104,86 +155,177 @@ const captions = {
} }
}, },
// Set the captions language // Toggle captions display
setLanguage() { // Used internally for the toggleCaptions method, with the passive option forced to false
// Setup HTML5 track rendering toggle(input, passive = true) {
if (this.isHTML5 && this.isVideo) { // If there's no full support
captions.getTracks.call(this).forEach(track => { if (!this.supported.ui) {
// Show track return;
utils.on(track, 'cuechange', event => captions.setCue.call(this, event)); }
// Turn off native caption rendering to avoid double captions const { toggled } = this.captions; // Current state
// eslint-disable-next-line const activeClass = this.config.classNames.captions.active;
track.mode = 'hidden';
});
// Get current track // Get the next state
const currentTrack = captions.getCurrentTrack.call(this); // If the method is called without parameter, toggle based on current value
const active = is.nullOrUndefined(input) ? !toggled : input;
// Check if suported kind // Update state and trigger event
if (utils.is.track(currentTrack)) { if (active !== toggled) {
// If we change the active track while a cue is already displayed we need to update it // When passive, don't override user preferences
if (Array.from(currentTrack.activeCues || []).length) { if (!passive) {
captions.setCue.call(this, currentTrack); this.captions.active = active;
} this.storage.set({ captions: active });
} }
} else if (this.isVimeo && this.captions.active) {
this.embed.enableTextTrack(this.language); // 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 state
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');
} }
}, },
// Get the tracks // Set captions by track index
getTracks() { // Used internally for the currentTrack setter with the passive option forced to false
// Return empty array at least set(index, passive = true) {
if (utils.is.nullOrUndefined(this.media)) {
return [];
}
// Only get accepted kinds
return Array.from(this.media.textTracks || []).filter(track => [
'captions',
'subtitles',
].includes(track.kind));
},
// Get the current track for the current language
getCurrentTrack() {
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
if (!tracks.length) { // Disable captions if setting to -1
return null; if (index === -1) {
captions.toggle.call(this, false, passive);
return;
} }
// Get track based on current language if (!is.number(index)) {
let track = tracks.find(track => track.language.toLowerCase() === this.language); this.debug.warn('Invalid caption argument', index);
return;
// Get the <track> with default attribute
if (!track) {
track = utils.getElement.call(this, 'track[default]');
} }
// Get the first track if (!(index in tracks)) {
if (!track) { this.debug.warn('Track not found', index);
[track] = tracks; return;
} }
return track; 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 });
}
// Handle Vimeo captions
if (this.isVimeo) {
this.embed.enableTextTrack(language);
}
// Trigger event
triggerEvent.call(this, this.media, 'languagechange');
}
// Show captions
captions.toggle.call(this, true, passive);
if (this.isHTML5 && this.isVideo) {
// If we change the active track while a cue is already displayed we need to update it
captions.updateCues.call(this);
}
},
// 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;
// Set currentTrack
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [language]);
captions.set.call(this, tracks.indexOf(track), passive);
},
// 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));
},
// 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);
},
// Get the current track
getCurrentTrack() {
return captions.getTracks.call(this)[this.currentTrack];
}, },
// Get UI label for track // Get UI label for track
getLabel(track) { getLabel(track) {
let currentTrack = track; let currentTrack = track;
if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) { if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
currentTrack = captions.getCurrentTrack.call(this); currentTrack = captions.getCurrentTrack.call(this);
} }
if (utils.is.track(currentTrack)) { if (is.track(currentTrack)) {
if (!utils.is.empty(currentTrack.label)) { if (!is.empty(currentTrack.label)) {
return currentTrack.label; return currentTrack.label;
} }
if (!utils.is.empty(currentTrack.language)) { if (!is.empty(currentTrack.language)) {
return track.language.toUpperCase(); return track.language.toUpperCase();
} }
@@ -193,56 +335,48 @@ const captions = {
return i18n.get('disabled', this.config); return i18n.get('disabled', this.config);
}, },
// Display active caption if it contains text // Update captions using current track's active cues
setCue(input) { // Also optional array argument in case there isn't any track (ex: vimeo)
// Get the track from the event if needed updateCues(input) {
const track = utils.is.event(input) ? input.target : input;
const { activeCues } = track;
const active = activeCues.length && activeCues[0];
const currentTrack = captions.getCurrentTrack.call(this);
// Only display current track
if (track !== currentTrack) {
return;
}
// Display a cue, if there is one
if (utils.is.cue(active)) {
captions.setText.call(this, active.getCueAsHTML());
} else {
captions.setText.call(this, null);
}
utils.dispatchEvent.call(this, this.media, 'cuechange');
},
// Set the current caption
setText(input) {
// Requires UI // Requires UI
if (!this.supported.ui) { if (!this.supported.ui) {
return; return;
} }
if (utils.is.element(this.elements.captions)) { if (!is.element(this.elements.captions)) {
const content = utils.createElement('span');
// Empty the container
utils.emptyElement(this.elements.captions);
// Default to empty
const caption = !utils.is.nullOrUndefined(input) ? input : '';
// Set the span content
if (utils.is.string(caption)) {
content.innerText = caption.trim();
} else {
content.appendChild(caption);
}
// Set new caption text
this.elements.captions.appendChild(content);
} else {
this.debug.warn('No captions element to render to'); 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');
} }
}, },
}; };
@@ -18,6 +18,10 @@ const defaults = {
// Only allow one media playing at once (vimeo only) // Only allow one media playing at once (vimeo only)
autopause: true, 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 // Default time to skip when rewind/fast forward
seekTime: 10, seekTime: 10,
@@ -56,7 +60,7 @@ const defaults = {
// Sprite (for icons) // Sprite (for icons)
loadSprite: true, loadSprite: true,
iconPrefix: 'plyr', iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.3.10/plyr.svg', iconUrl: 'https://cdn.plyr.io/3.3.12/plyr.svg',
// Blank video (used to prevent errors on source change) // Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4', blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@@ -89,15 +93,7 @@ const defaults = {
// Speed default and options to display // Speed default and options to display
speed: { speed: {
selected: 1, selected: 1,
options: [ options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
0.5,
0.75,
1,
1.25,
1.5,
1.75,
2,
],
}, },
// Keyboard shortcut settings // Keyboard shortcut settings
@@ -151,11 +147,7 @@ const defaults = {
'airplay', 'airplay',
'fullscreen', 'fullscreen',
], ],
settings: [ settings: ['captions', 'quality', 'speed'],
'captions',
'quality',
'speed',
],
// Localisation // Localisation
i18n: { i18n: {
@@ -165,6 +157,7 @@ const defaults = {
pause: 'Pause', pause: 'Pause',
fastForward: 'Forward {seektime}s', fastForward: 'Forward {seektime}s',
seek: 'Seek', seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
played: 'Played', played: 'Played',
buffered: 'Buffered', buffered: 'Buffered',
currentTime: 'Current time', currentTime: 'Current time',
@@ -179,6 +172,7 @@ const defaults = {
frameTitle: 'Player for {title}', frameTitle: 'Player for {title}',
captions: 'Captions', captions: 'Captions',
settings: 'Settings', settings: 'Settings',
menuBack: 'Go back to previous menu',
speed: 'Speed', speed: 'Speed',
normal: 'Normal', normal: 'Normal',
quality: 'Quality', quality: 'Quality',
@@ -209,7 +203,8 @@ const defaults = {
}, },
youtube: { youtube: {
sdk: 'https://www.youtube.com/iframe_api', sdk: 'https://www.youtube.com/iframe_api',
api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet', api:
'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
}, },
googleIMA: { googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
@@ -328,6 +323,7 @@ const defaults = {
}, },
progress: '.plyr__progress', progress: '.plyr__progress',
captions: '.plyr__captions', captions: '.plyr__captions',
caption: '.plyr__caption',
menu: { menu: {
quality: '.js-plyr__menu__list--quality', quality: '.js-plyr__menu__list--quality',
}, },
@@ -344,6 +340,7 @@ const defaults = {
posterEnabled: 'plyr__poster-enabled', posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads', ads: 'plyr__ads',
control: 'plyr__control', control: 'plyr__control',
controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing', playing: 'plyr--playing',
paused: 'plyr--paused', paused: 'plyr--paused',
stopped: 'plyr--stopped', stopped: 'plyr--stopped',
@@ -13,4 +13,22 @@ export const types = {
video: 'video', video: 'video',
}; };
/**
* Get provider by URL
* @param {string} url
*/
export function getProviderByUrl(url) {
// YouTube
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) {
return providers.youtube;
}
// Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo;
}
return null;
}
export default { providers, types }; export default { providers, types };
+317 -319
View File
File diff suppressed because it is too large Load Diff
+36 -27
View File
@@ -3,9 +3,10 @@
// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing // https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing
// ========================================================================== // ==========================================================================
import utils from './utils'; import browser from './utils/browser';
import { hasClass, toggleClass, trapFocus } from './utils/elements';
const browser = utils.getBrowser(); import { on, triggerEvent } from './utils/events';
import is from './utils/is';
function onChange() { function onChange() {
if (!this.enabled) { if (!this.enabled) {
@@ -14,16 +15,16 @@ function onChange() {
// Update toggle button // Update toggle button
const button = this.player.elements.buttons.fullscreen; const button = this.player.elements.buttons.fullscreen;
if (utils.is.element(button)) { if (is.element(button)) {
utils.toggleState(button, this.active); button.pressed = this.active;
} }
// Trigger an event // Trigger an event
utils.dispatchEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
// Trap focus in container // Trap focus in container
if (!browser.isIos) { if (!browser.isIos) {
utils.trapFocus.call(this.player, this.target, this.active); trapFocus.call(this.player, this.target, this.active);
} }
} }
@@ -42,7 +43,7 @@ function toggleFallback(toggle = false) {
document.body.style.overflow = toggle ? 'hidden' : ''; document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook // Toggle class hook
utils.toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Toggle button and fire events // Toggle button and fire events
onChange.call(this); onChange.call(this);
@@ -62,15 +63,20 @@ class Fullscreen {
// Register event listeners // Register event listeners
// Handle event (incase user presses escape etc) // Handle event (incase user presses escape etc)
utils.on(document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => { on.call(
// TODO: Filter for target?? this.player,
onChange.call(this); document,
}); this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => {
// TODO: Filter for target??
onChange.call(this);
},
);
// Fullscreen toggle on double click // Fullscreen toggle on double click
utils.on(this.player.elements.container, 'dblclick', event => { on.call(this.player, this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls // Ignore double click in controls
if (utils.is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) { if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return; return;
} }
@@ -83,26 +89,27 @@ class Fullscreen {
// Determine if native supported // Determine if native supported
static get native() { static get native() {
return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled); return !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled
);
} }
// Get the prefix for handlers // Get the prefix for handlers
static get prefix() { static get prefix() {
// No prefix // No prefix
if (utils.is.function(document.exitFullscreen)) { if (is.function(document.exitFullscreen)) {
return ''; return '';
} }
// Check for fullscreen support by vendor prefix // Check for fullscreen support by vendor prefix
let value = ''; let value = '';
const prefixes = [ const prefixes = ['webkit', 'moz', 'ms'];
'webkit',
'moz',
'ms',
];
prefixes.some(pre => { prefixes.some(pre => {
if (utils.is.function(document[`${pre}ExitFullscreen`]) || utils.is.function(document[`${pre}CancelFullScreen`])) { if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
value = pre; value = pre;
return true; return true;
} }
@@ -135,7 +142,7 @@ class Fullscreen {
// Fallback using classname // Fallback using classname
if (!Fullscreen.native) { if (!Fullscreen.native) {
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback); return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
} }
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`]; const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
@@ -145,7 +152,9 @@ class Fullscreen {
// Get target element // Get target element
get target() { get target() {
return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container; return browser.isIos && this.player.config.fullscreen.iosNative
? this.player.media
: this.player.elements.container;
} }
// Update UI // Update UI
@@ -157,7 +166,7 @@ class Fullscreen {
} }
// Add styling hook to show button // Add styling hook to show button
utils.toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled); toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
} }
// Make an element fullscreen // Make an element fullscreen
@@ -175,7 +184,7 @@ class Fullscreen {
toggleFallback.call(this, true); toggleFallback.call(this, true);
} else if (!this.prefix) { } else if (!this.prefix) {
this.target.requestFullscreen(); this.target.requestFullscreen();
} else if (!utils.is.empty(this.prefix)) { } else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`](); this.target[`${this.prefix}Request${this.property}`]();
} }
} }
@@ -194,7 +203,7 @@ class Fullscreen {
toggleFallback.call(this, false); toggleFallback.call(this, false);
} else if (!this.prefix) { } else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document); (document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!utils.is.empty(this.prefix)) { } else if (!is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit'; const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.property}`](); document[`${this.prefix}${action}${this.property}`]();
} }
+36 -72
View File
@@ -3,40 +3,28 @@
// ========================================================================== // ==========================================================================
import support from './support'; import support from './support';
import utils from './utils'; import { removeElement } from './utils/elements';
import { triggerEvent } from './utils/events';
const html5 = { const html5 = {
getSources() { getSources() {
if (!this.isHTML5) { if (!this.isHTML5) {
return null; return [];
} }
return this.media.querySelectorAll('source'); const sources = Array.from(this.media.querySelectorAll('source'));
// Filter out unsupported sources
return sources.filter(source => support.mime.call(this, source.getAttribute('type')));
}, },
// Get quality levels // Get quality levels
getQualityOptions() { getQualityOptions() {
if (!this.isHTML5) { // Get sizes from <source> elements
return null; return html5.getSources
} .call(this)
.map(source => Number(source.getAttribute('size')))
// Get sources .filter(Boolean);
const sources = html5.getSources.call(this);
if (utils.is.empty(sources)) {
return null;
}
// Get <source> with size attribute
const sizes = Array.from(sources).filter(source => !utils.is.empty(source.getAttribute('size')));
// If none, bail
if (utils.is.empty(sizes)) {
return null;
}
// Reduce to unique list
return utils.dedupe(sizes.map(source => Number(source.getAttribute('size'))));
}, },
extend() { extend() {
@@ -51,71 +39,47 @@ const html5 = {
get() { get() {
// Get sources // Get sources
const sources = html5.getSources.call(player); const sources = html5.getSources.call(player);
const source = sources.find(source => source.getAttribute('src') === player.source);
if (utils.is.empty(sources)) { // Return size, if match is found
return null; return source && Number(source.getAttribute('size'));
}
const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
if (utils.is.empty(matches)) {
return null;
}
return Number(matches[0].getAttribute('size'));
}, },
set(input) { set(input) {
// Get sources // Get sources
const sources = html5.getSources.call(player); const sources = html5.getSources.call(player);
if (utils.is.empty(sources)) { // Get first match for requested size
const source = sources.find(source => Number(source.getAttribute('size')) === input);
// No matching source found
if (!source) {
return; return;
} }
// Get matches for requested size
const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input);
// No matches for requested size
if (utils.is.empty(matches)) {
return;
}
// Get supported sources
const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type')));
// No supported sources
if (utils.is.empty(supported)) {
return;
}
// Trigger change event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
quality: input,
});
// Get current state // Get current state
const { currentTime, playing } = player; const { currentTime, paused, preload, readyState } = player.media;
// Set new source // Set new source
player.media.src = supported[0].getAttribute('src'); player.media.src = source.getAttribute('src');
// Restore time // Prevent loading if preload="none" and the current source isn't loaded (#1044)
const onLoadedMetaData = () => { if (preload !== 'none' || readyState) {
player.currentTime = currentTime; // Restore time
player.off('loadedmetadata', onLoadedMetaData); player.once('loadedmetadata', () => {
}; player.currentTime = currentTime;
player.on('loadedmetadata', onLoadedMetaData);
// Load new source // Resume playing
player.media.load(); if (!paused) {
player.play();
}
});
// Resume playing // Load new source
if (playing) { player.media.load();
player.play();
} }
// Trigger change event // Trigger change event
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, { triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: input, quality: input,
}); });
}, },
@@ -130,7 +94,7 @@ const html5 = {
} }
// Remove child sources // Remove child sources
utils.removeElement(html5.getSources()); removeElement(html5.getSources.call(this));
// Set blank video src attribute // Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
+8 -9
View File
@@ -2,17 +2,19 @@
// Plyr internationalization // Plyr internationalization
// ========================================================================== // ==========================================================================
import utils from './utils'; import is from './utils/is';
import { getDeep } from './utils/objects';
import { replaceAll } from './utils/strings';
const i18n = { const i18n = {
get(key = '', config = {}) { get(key = '', config = {}) {
if (utils.is.empty(key) || utils.is.empty(config)) { if (is.empty(key) || is.empty(config)) {
return ''; return '';
} }
let string = utils.getDeep(config.i18n, key); let string = getDeep(config.i18n, key);
if (utils.is.empty(string)) { if (is.empty(string)) {
return ''; return '';
} }
@@ -21,11 +23,8 @@ const i18n = {
'{title}': config.title, '{title}': config.title,
}; };
Object.entries(replace).forEach(([ Object.entries(replace).forEach(([key, value]) => {
key, string = replaceAll(string, key, value);
value,
]) => {
string = utils.replaceAll(string, key, value);
}); });
return string; return string;
+146 -183
View File
@@ -4,10 +4,10 @@
import controls from './controls'; import controls from './controls';
import ui from './ui'; import ui from './ui';
import utils from './utils'; import browser from './utils/browser';
import { getElement, getElements, getFocusElement, matches, toggleClass, toggleHidden } from './utils/elements';
// Sniff out the browser import { on, once, toggleListener, triggerEvent } from './utils/events';
const browser = utils.getBrowser(); import is from './utils/is';
class Listeners { class Listeners {
constructor(player) { constructor(player) {
@@ -32,7 +32,7 @@ class Listeners {
// If the event is bubbled from the media element // If the event is bubbled from the media element
// Firefox doesn't get the keycode for whatever reason // Firefox doesn't get the keycode for whatever reason
if (!utils.is.number(code)) { if (!is.number(code)) {
return; return;
} }
@@ -46,35 +46,17 @@ class Listeners {
// Reset on keyup // Reset on keyup
if (pressed) { if (pressed) {
// Which keycodes should we prevent default // Which keycodes should we prevent default
const preventDefault = [ const preventDefault = [32, 37, 38, 39, 40, 48, 49, 50, 51, 52, 53, 54, 56, 57, 67, 70, 73, 75, 76, 77, 79];
48,
49,
50,
51,
52,
53,
54,
56,
57,
32,
75,
38,
40,
77,
39,
37,
70,
67,
73,
76,
79,
];
// Check focused element // Check focused element
// and if the focused element is not editable (e.g. text input) // and if the focused element is not editable (e.g. text input)
// and any that accept key input http://webaim.org/techniques/keyboard/ // and any that accept key input http://webaim.org/techniques/keyboard/
const focused = utils.getFocusElement(); const focused = getFocusElement();
if (utils.is.element(focused) && utils.matches(focused, this.player.config.selectors.editable)) { if (
is.element(focused) &&
(focused !== this.player.elements.inputs.seek &&
matches(focused, this.player.config.selectors.editable))
) {
return; return;
} }
@@ -192,41 +174,37 @@ class Listeners {
this.player.touch = true; this.player.touch = true;
// Add touch class // Add touch class
utils.toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true); toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
// Clean up
utils.off(document.body, 'touchstart', this.firstTouch);
} }
// Global window & document listeners // Global window & document listeners
global(toggle = true) { global(toggle = true) {
// Keyboard shortcuts // Keyboard shortcuts
if (this.player.config.keyboard.global) { if (this.player.config.keyboard.global) {
utils.toggleListener(window, 'keydown keyup', this.handleKey, toggle, false); toggleListener.call(this.player, window, 'keydown keyup', this.handleKey, toggle, false);
} }
// Click anywhere closes menu // Click anywhere closes menu
utils.toggleListener(document.body, 'click', this.toggleMenu, toggle); toggleListener.call(this.player, document.body, 'click', this.toggleMenu, toggle);
// Detect touch by events // Detect touch by events
utils.on(document.body, 'touchstart', this.firstTouch); once.call(this.player, document.body, 'touchstart', this.firstTouch);
} }
// Container listeners // Container listeners
container() { container() {
// Keyboard shortcuts // Keyboard shortcuts
if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) { if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) {
utils.on(this.player.elements.container, 'keydown keyup', this.handleKey, false); on.call(this.player, this.player.elements.container, 'keydown keyup', this.handleKey, false);
} }
// Detect tab focus // Detect tab focus
// Remove class on blur/focusout // Remove class on blur/focusout
utils.on(this.player.elements.container, 'focusout', event => { on.call(this.player, this.player.elements.container, 'focusout', event => {
utils.toggleClass(event.target, this.player.config.classNames.tabFocus, false); toggleClass(event.target, this.player.config.classNames.tabFocus, false);
}); });
// Add classname to tabbed elements // Add classname to tabbed elements
utils.on(this.player.elements.container, 'keydown', event => { on.call(this.player, this.player.elements.container, 'keydown', event => {
if (event.keyCode !== 9) { if (event.keyCode !== 9) {
return; return;
} }
@@ -234,59 +212,64 @@ class Listeners {
// Delay the adding of classname until the focus has changed // Delay the adding of classname until the focus has changed
// This event fires before the focusin event // This event fires before the focusin event
setTimeout(() => { setTimeout(() => {
utils.toggleClass(utils.getFocusElement(), this.player.config.classNames.tabFocus, true); toggleClass(getFocusElement(), this.player.config.classNames.tabFocus, true);
}, 0); }, 0);
}); });
// Toggle controls on mouse events and entering fullscreen // Toggle controls on mouse events and entering fullscreen
utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => { on.call(
const { controls } = this.player.elements; this.player,
this.player.elements.container,
'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',
event => {
const { controls } = this.player.elements;
// Remove button states for fullscreen // Remove button states for fullscreen
if (event.type === 'enterfullscreen') { if (event.type === 'enterfullscreen') {
controls.pressed = false; controls.pressed = false;
controls.hover = false; controls.hover = false;
} }
// Show, then hide after a timeout unless another control event occurs // Show, then hide after a timeout unless another control event occurs
const show = [ const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
'touchstart',
'touchmove',
'mousemove',
].includes(event.type);
let delay = 0; let delay = 0;
if (show) { if (show) {
ui.toggleControls.call(this.player, true); ui.toggleControls.call(this.player, true);
// Use longer timeout for touch devices // Use longer timeout for touch devices
delay = this.player.touch ? 3000 : 2000; delay = this.player.touch ? 3000 : 2000;
} }
// Clear timer // Clear timer
clearTimeout(this.player.timers.controls); clearTimeout(this.player.timers.controls);
// Timer to prevent flicker when seeking // Timer to prevent flicker when seeking
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay); this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
}); },
);
} }
// Listen for media events // Listen for media events
media() { media() {
// Time change on media // Time change on media
utils.on(this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event)); on.call(this.player, this.player.media, 'timeupdate seeking seeked', event =>
controls.timeUpdate.call(this.player, event),
);
// Display duration // Display duration
utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event)); on.call(this.player, this.player.media, 'durationchange loadeddata loadedmetadata', event =>
controls.durationUpdate.call(this.player, event),
);
// Check for audio tracks on load // Check for audio tracks on load
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
utils.on(this.player.media, 'loadeddata', () => { on.call(this.player, this.player.media, 'canplay', () => {
utils.toggleHidden(this.player.elements.volume, !this.player.hasAudio); toggleHidden(this.player.elements.volume, !this.player.hasAudio);
utils.toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio); toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio);
}); });
// Handle the media finishing // Handle the media finishing
utils.on(this.player.media, 'ended', () => { on.call(this.player, this.player.media, 'ended', () => {
// Show poster on end // Show poster on end
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) { if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
// Restart // Restart
@@ -295,20 +278,28 @@ class Listeners {
}); });
// Check for buffer progress // Check for buffer progress
utils.on(this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event)); on.call(this.player, this.player.media, 'progress playing seeking seeked', event =>
controls.updateProgress.call(this.player, event),
);
// Handle volume changes // Handle volume changes
utils.on(this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event)); on.call(this.player, this.player.media, 'volumechange', event =>
controls.updateVolume.call(this.player, event),
);
// Handle play/pause // Handle play/pause
utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event)); on.call(this.player, this.player.media, 'playing play pause ended emptied timeupdate', event =>
ui.checkPlaying.call(this.player, event),
);
// Loading state // Loading state
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event)); on.call(this.player, this.player.media, 'waiting canplay seeked playing', event =>
ui.checkLoading.call(this.player, event),
);
// If autoplay, then load advertisement if required // If autoplay, then load advertisement if required
// TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
utils.on(this.player.media, 'playing', () => { on.call(this.player, this.player.media, 'playing', () => {
if (!this.player.ads) { if (!this.player.ads) {
return; return;
} }
@@ -323,15 +314,15 @@ class Listeners {
// Click video // Click video
if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) { if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) {
// Re-fetch the wrapper // Re-fetch the wrapper
const wrapper = utils.getElement.call(this.player, `.${this.player.config.classNames.video}`); const wrapper = getElement.call(this.player, `.${this.player.config.classNames.video}`);
// Bail if there's no wrapper (this should never happen) // Bail if there's no wrapper (this should never happen)
if (!utils.is.element(wrapper)) { if (!is.element(wrapper)) {
return; return;
} }
// On click play, pause ore restart // On click play, pause ore restart
utils.on(wrapper, 'click', () => { on.call(this.player, wrapper, 'click', () => {
// Touch devices will just show controls (if we're hiding controls) // Touch devices will just show controls (if we're hiding controls)
if (this.player.config.hideControls && this.player.touch && !this.player.paused) { if (this.player.config.hideControls && this.player.touch && !this.player.paused) {
return; return;
@@ -350,7 +341,8 @@ class Listeners {
// Disable right click // Disable right click
if (this.player.supported.ui && this.player.config.disableContextMenu) { if (this.player.supported.ui && this.player.config.disableContextMenu) {
utils.on( on.call(
this.player,
this.player.elements.wrapper, this.player.elements.wrapper,
'contextmenu', 'contextmenu',
event => { event => {
@@ -361,13 +353,13 @@ class Listeners {
} }
// Volume change // Volume change
utils.on(this.player.media, 'volumechange', () => { on.call(this.player, this.player.media, 'volumechange', () => {
// Save to storage // Save to storage
this.player.storage.set({ volume: this.player.volume, muted: this.player.muted }); this.player.storage.set({ volume: this.player.volume, muted: this.player.muted });
}); });
// Speed change // Speed change
utils.on(this.player.media, 'ratechange', () => { on.call(this.player, this.player.media, 'ratechange', () => {
// Update UI // Update UI
controls.updateSetting.call(this.player, 'speed'); controls.updateSetting.call(this.player, 'speed');
@@ -376,49 +368,29 @@ class Listeners {
}); });
// Quality request // Quality request
utils.on(this.player.media, 'qualityrequested', event => { on.call(this.player, this.player.media, 'qualityrequested', event => {
// Save to storage // Save to storage
this.player.storage.set({ quality: event.detail.quality }); this.player.storage.set({ quality: event.detail.quality });
}); });
// Quality change // Quality change
utils.on(this.player.media, 'qualitychange', event => { on.call(this.player, this.player.media, 'qualitychange', event => {
// Update UI // Update UI
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality); controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
}); });
// Caption language change
utils.on(this.player.media, 'languagechange', () => {
// Update UI
controls.updateSetting.call(this.player, 'captions');
// Save to storage
this.player.storage.set({ language: this.player.language });
});
// Captions toggle
utils.on(this.player.media, 'captionsenabled captionsdisabled', () => {
// Update UI
controls.updateSetting.call(this.player, 'captions');
// Save to storage
this.player.storage.set({ captions: this.player.captions.active });
});
// Proxy events to container // Proxy events to container
// Bubble up key events for Edge // Bubble up key events for Edge
utils.on(this.player.media, this.player.config.events.concat([ const proxyEvents = this.player.config.events.concat(['keyup', 'keydown']).join(' ');
'keyup', on.call(this.player, this.player.media, proxyEvents, event => {
'keydown', let { detail = {} } = event;
]).join(' '), event => {
let detail = {};
// Get error details from media // Get error details from media
if (event.type === 'error') { if (event.type === 'error') {
detail = this.player.media.error; detail = this.player.media.error;
} }
utils.dispatchEvent.call(this.player, this.player.elements.container, event.type, true, detail); triggerEvent.call(this.player, this.player.elements.container, event.type, true, detail);
}); });
} }
@@ -430,7 +402,7 @@ class Listeners {
// Run default and custom handlers // Run default and custom handlers
const proxy = (event, defaultHandler, customHandlerKey) => { const proxy = (event, defaultHandler, customHandlerKey) => {
const customHandler = this.player.config.listeners[customHandlerKey]; const customHandler = this.player.config.listeners[customHandlerKey];
const hasCustomHandler = utils.is.function(customHandler); const hasCustomHandler = is.function(customHandler);
let returned = true; let returned = true;
// Execute custom handler // Execute custom handler
@@ -439,33 +411,41 @@ class Listeners {
} }
// Only call default handler if not prevented in custom handler // Only call default handler if not prevented in custom handler
if (returned && utils.is.function(defaultHandler)) { if (returned && is.function(defaultHandler)) {
defaultHandler.call(this.player, event); defaultHandler.call(this.player, event);
} }
}; };
// Trigger custom and default handlers // Trigger custom and default handlers
const on = (element, type, defaultHandler, customHandlerKey, passive = true) => { const bind = (element, type, defaultHandler, customHandlerKey, passive = true) => {
const customHandler = this.player.config.listeners[customHandlerKey]; const customHandler = this.player.config.listeners[customHandlerKey];
const hasCustomHandler = utils.is.function(customHandler); const hasCustomHandler = is.function(customHandler);
utils.on(element, type, event => proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler); on.call(
this.player,
element,
type,
event => proxy(event, defaultHandler, customHandlerKey),
passive && !hasCustomHandler,
);
}; };
// Play/pause toggle // Play/pause toggle
on(this.player.elements.buttons.play, 'click', this.player.togglePlay, 'play'); Array.from(this.player.elements.buttons.play).forEach(button => {
bind(button, 'click', this.player.togglePlay, 'play');
});
// Pause // Pause
on(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart'); bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
// Rewind // Rewind
on(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind'); bind(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind');
// Rewind // Rewind
on(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward'); bind(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward');
// Mute toggle // Mute toggle
on( bind(
this.player.elements.buttons.mute, this.player.elements.buttons.mute,
'click', 'click',
() => { () => {
@@ -475,10 +455,10 @@ class Listeners {
); );
// Captions toggle // Captions toggle
on(this.player.elements.buttons.captions, 'click', this.player.toggleCaptions); bind(this.player.elements.buttons.captions, 'click', () => this.player.toggleCaptions());
// Fullscreen toggle // Fullscreen toggle
on( bind(
this.player.elements.buttons.fullscreen, this.player.elements.buttons.fullscreen,
'click', 'click',
() => { () => {
@@ -488,7 +468,7 @@ class Listeners {
); );
// Picture-in-Picture // Picture-in-Picture
on( bind(
this.player.elements.buttons.pip, this.player.elements.buttons.pip,
'click', 'click',
() => { () => {
@@ -498,15 +478,15 @@ class Listeners {
); );
// Airplay // Airplay
on(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay'); bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay');
// Settings menu // Settings menu
on(this.player.elements.buttons.settings, 'click', event => { bind(this.player.elements.buttons.settings, 'click', event => {
controls.toggleMenu.call(this.player, event); controls.toggleMenu.call(this.player, event);
}); });
// Settings menu // Settings menu
on(this.player.elements.settings.form, 'click', event => { bind(this.player.elements.settings.form, 'click', event => {
event.stopPropagation(); event.stopPropagation();
// Go back to home tab on click // Go back to home tab on click
@@ -516,16 +496,16 @@ class Listeners {
}; };
// Settings menu items - use event delegation as items are added/removed // Settings menu items - use event delegation as items are added/removed
if (utils.matches(event.target, this.player.config.selectors.inputs.language)) { if (matches(event.target, this.player.config.selectors.inputs.language)) {
proxy( proxy(
event, event,
() => { () => {
this.player.language = event.target.value; this.player.currentTrack = Number(event.target.value);
showHomeTab(); showHomeTab();
}, },
'language', 'language',
); );
} else if (utils.matches(event.target, this.player.config.selectors.inputs.quality)) { } else if (matches(event.target, this.player.config.selectors.inputs.quality)) {
proxy( proxy(
event, event,
() => { () => {
@@ -534,7 +514,7 @@ class Listeners {
}, },
'quality', 'quality',
); );
} else if (utils.matches(event.target, this.player.config.selectors.inputs.speed)) { } else if (matches(event.target, this.player.config.selectors.inputs.speed)) {
proxy( proxy(
event, event,
() => { () => {
@@ -550,25 +530,27 @@ class Listeners {
}); });
// Set range input alternative "value", which matches the tooltip time (#954) // Set range input alternative "value", which matches the tooltip time (#954)
on(this.player.elements.inputs.seek, 'mousedown mousemove', event => { bind(this.player.elements.inputs.seek, 'mousedown mousemove', event => {
const clientRect = this.player.elements.progress.getBoundingClientRect(); const clientRect = this.player.elements.progress.getBoundingClientRect();
const percent = 100 / clientRect.width * (event.pageX - clientRect.left); const percent = 100 / clientRect.width * (event.pageX - clientRect.left);
event.currentTarget.setAttribute('seek-value', percent); event.currentTarget.setAttribute('seek-value', percent);
}); });
// Pause while seeking // Pause while seeking
on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { bind(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
const seek = event.currentTarget; const seek = event.currentTarget;
const code = event.keyCode ? event.keyCode : event.which;
const eventType = event.type;
if ((eventType === 'keydown' || eventType === 'keyup') && (code !== 39 && code !== 37)) {
return;
}
// Was playing before? // Was playing before?
const play = seek.hasAttribute('play-on-seeked'); const play = seek.hasAttribute('play-on-seeked');
// Done seeking // Done seeking
const done = [ const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
'mouseup',
'touchend',
'keyup',
].includes(event.type);
// If we're done seeking and it was playing, resume playback // If we're done seeking and it was playing, resume playback
if (play && done) { if (play && done) {
@@ -581,7 +563,7 @@ class Listeners {
}); });
// Seek // Seek
on( bind(
this.player.elements.inputs.seek, this.player.elements.inputs.seek,
inputEvent, inputEvent,
event => { event => {
@@ -590,7 +572,7 @@ class Listeners {
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954) // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
let seekTo = seek.getAttribute('seek-value'); let seekTo = seek.getAttribute('seek-value');
if (utils.is.empty(seekTo)) { if (is.empty(seekTo)) {
seekTo = seek.value; seekTo = seek.value;
} }
@@ -603,8 +585,8 @@ class Listeners {
// Current time invert // Current time invert
// Only if one time element is used for both currentTime and duration // Only if one time element is used for both currentTime and duration
if (this.player.config.toggleInvert && !utils.is.element(this.player.elements.display.duration)) { if (this.player.config.toggleInvert && !is.element(this.player.elements.display.duration)) {
on(this.player.elements.display.currentTime, 'click', () => { bind(this.player.elements.display.currentTime, 'click', () => {
// Do nothing if we're at the start // Do nothing if we're at the start
if (this.player.currentTime === 0) { if (this.player.currentTime === 0) {
return; return;
@@ -617,7 +599,7 @@ class Listeners {
} }
// Volume // Volume
on( bind(
this.player.elements.inputs.volume, this.player.elements.inputs.volume,
inputEvent, inputEvent,
event => { event => {
@@ -628,33 +610,32 @@ class Listeners {
// Polyfill for lower fill in <input type="range"> for webkit // Polyfill for lower fill in <input type="range"> for webkit
if (browser.isWebkit) { if (browser.isWebkit) {
on(utils.getElements.call(this.player, 'input[type="range"]'), 'input', event => { Array.from(getElements.call(this.player, 'input[type="range"]')).forEach(element => {
controls.updateRangeFill.call(this.player, event.target); bind(element, 'input', event => controls.updateRangeFill.call(this.player, event.target));
}); });
} }
// Seek tooltip // Seek tooltip
on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event)); bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', event =>
controls.updateSeekTooltip.call(this.player, event),
);
// Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting) // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
on(this.player.elements.controls, 'mouseenter mouseleave', event => { bind(this.player.elements.controls, 'mouseenter mouseleave', event => {
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter'; this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
}); });
// Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { bind(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
this.player.elements.controls.pressed = [ this.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
'mousedown',
'touchstart',
].includes(event.type);
}); });
// Focus in/out on controls // Focus in/out on controls
on(this.player.elements.controls, 'focusin focusout', event => { bind(this.player.elements.controls, 'focusin focusout', event => {
const { config, elements, timers } = this.player; const { config, elements, timers } = this.player;
// Skip transition to prevent focus from scrolling the parent element // Skip transition to prevent focus from scrolling the parent element
utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin'); toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin');
// Toggle // Toggle
ui.toggleControls.call(this.player, event.type === 'focusin'); ui.toggleControls.call(this.player, event.type === 'focusin');
@@ -663,7 +644,7 @@ class Listeners {
if (event.type === 'focusin') { if (event.type === 'focusin') {
// Restore transition // Restore transition
setTimeout(() => { setTimeout(() => {
utils.toggleClass(elements.controls, config.classNames.noTransition, false); toggleClass(elements.controls, config.classNames.noTransition, false);
}, 0); }, 0);
// Delay a little more for keyboard users // Delay a little more for keyboard users
@@ -677,40 +658,27 @@ class Listeners {
}); });
// Mouse wheel for volume // Mouse wheel for volume
on( bind(
this.player.elements.inputs.volume, this.player.elements.inputs.volume,
'wheel', 'wheel',
event => { event => {
// Detect "natural" scroll - suppored on OS X Safari only // Detect "natural" scroll - suppored on OS X Safari only
// Other browsers on OS X will be inverted until support improves // Other browsers on OS X will be inverted until support improves
const inverted = event.webkitDirectionInvertedFromDevice; const inverted = event.webkitDirectionInvertedFromDevice;
const step = 1 / 50;
let direction = 0;
// Scroll down (or up on natural) to decrease // Get delta from event. Invert if `inverted` is true
if (event.deltaY < 0 || event.deltaX > 0) { const [x, y] = [event.deltaX, -event.deltaY]
if (inverted) { .map(value => inverted ? -value : value);
this.player.decreaseVolume(step);
direction = -1;
} else {
this.player.increaseVolume(step);
direction = 1;
}
}
// Scroll up (or down on natural) to increase // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)
if (event.deltaY > 0 || event.deltaX < 0) { const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);
if (inverted) {
this.player.increaseVolume(step); // Change the volume by 2%
direction = 1; this.player.increaseVolume(direction / 50);
} else {
this.player.decreaseVolume(step);
direction = -1;
}
}
// Don't break page scrolling at max and min // Don't break page scrolling at max and min
if ((direction === 1 && this.player.media.volume < 1) || (direction === -1 && this.player.media.volume > 0)) { const { volume } = this.player.media;
if ((direction === 1 && volume < 1) || (direction === -1 && volume > 0)) {
event.preventDefault(); event.preventDefault();
} }
}, },
@@ -718,11 +686,6 @@ class Listeners {
false, false,
); );
} }
// Reset on destroy
clear() {
this.global(false);
}
} }
export default Listeners; export default Listeners;
+12 -21
View File
@@ -5,7 +5,7 @@
import html5 from './html5'; import html5 from './html5';
import vimeo from './plugins/vimeo'; import vimeo from './plugins/vimeo';
import youtube from './plugins/youtube'; import youtube from './plugins/youtube';
import utils from './utils'; import { createElement, toggleClass, wrap } from './utils/elements';
const media = { const media = {
// Setup media // Setup media
@@ -17,50 +17,41 @@ const media = {
} }
// Add type class // Add type class
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true); toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
// Add provider class // Add provider class
utils.toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true); toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
// Add video class for embeds // Add video class for embeds
// This will require changes if audio embeds are added // This will require changes if audio embeds are added
if (this.isEmbed) { if (this.isEmbed) {
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true); toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
} }
// Inject the player wrapper // Inject the player wrapper
if (this.isVideo) { if (this.isVideo) {
// Create the wrapper div // Create the wrapper div
this.elements.wrapper = utils.createElement('div', { this.elements.wrapper = createElement('div', {
class: this.config.classNames.video, class: this.config.classNames.video,
}); });
// Wrap the video in a container // Wrap the video in a container
utils.wrap(this.media, this.elements.wrapper); wrap(this.media, this.elements.wrapper);
// Faux poster container // Faux poster container
this.elements.poster = utils.createElement('div', { this.elements.poster = createElement('div', {
class: this.config.classNames.poster, class: this.config.classNames.poster,
}); });
this.elements.wrapper.appendChild(this.elements.poster); this.elements.wrapper.appendChild(this.elements.poster);
} }
if (this.isEmbed) { if (this.isHTML5) {
switch (this.provider) {
case 'youtube':
youtube.setup.call(this);
break;
case 'vimeo':
vimeo.setup.call(this);
break;
default:
break;
}
} else if (this.isHTML5) {
html5.extend.call(this); html5.extend.call(this);
} else if (this.isYouTube) {
youtube.setup.call(this);
} else if (this.isVimeo) {
vimeo.setup.call(this);
} }
}, },
}; };
+28 -18
View File
@@ -7,7 +7,12 @@
/* global google */ /* global google */
import i18n from '../i18n'; import i18n from '../i18n';
import utils from '../utils'; import { createElement } from './../utils/elements';
import { triggerEvent } from './../utils/events';
import is from './../utils/is';
import loadScript from './../utils/loadScript';
import { formatTime } from './../utils/time';
import { buildUrlParams } from './../utils/urls';
class Ads { class Ads {
/** /**
@@ -44,7 +49,9 @@ class Ads {
} }
get enabled() { get enabled() {
return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId); return (
this.player.isHTML5 && this.player.isVideo && this.player.config.ads.enabled && !is.empty(this.publisherId)
);
} }
/** /**
@@ -53,9 +60,8 @@ class Ads {
load() { load() {
if (this.enabled) { if (this.enabled) {
// Check if the Google IMA3 SDK is loaded or load it ourselves // Check if the Google IMA3 SDK is loaded or load it ourselves
if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) { if (!is.object(window.google) || !is.object(window.google.ima)) {
utils loadScript(this.player.config.urls.googleIMA.sdk)
.loadScript(this.player.config.urls.googleIMA.sdk)
.then(() => { .then(() => {
this.ready(); this.ready();
}) })
@@ -103,7 +109,7 @@ class Ads {
const base = 'https://go.aniview.com/api/adserver6/vast/'; const base = 'https://go.aniview.com/api/adserver6/vast/';
return `${base}?${utils.buildUrlParams(params)}`; return `${base}?${buildUrlParams(params)}`;
} }
/** /**
@@ -116,7 +122,7 @@ class Ads {
*/ */
setupIMA() { setupIMA() {
// Create the container for our advertisements // Create the container for our advertisements
this.elements.container = utils.createElement('div', { this.elements.container = createElement('div', {
class: this.player.config.classNames.ads, class: this.player.config.classNames.ads,
}); });
this.player.elements.container.appendChild(this.elements.container); this.player.elements.container.appendChild(this.elements.container);
@@ -146,7 +152,11 @@ class Ads {
this.loader = new google.ima.AdsLoader(this.elements.displayContainer); this.loader = new google.ima.AdsLoader(this.elements.displayContainer);
// Listen and respond to ads loaded and error events // Listen and respond to ads loaded and error events
this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false); this.loader.addEventListener(
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
event => this.onAdsManagerLoaded(event),
false,
);
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false); this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);
// Request video ads // Request video ads
@@ -184,7 +194,7 @@ class Ads {
} }
const update = () => { const update = () => {
const time = utils.formatTime(Math.max(this.manager.getRemainingTime(), 0)); const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));
const label = `${i18n.get('advertisement', this.player.config)} - ${time}`; const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;
this.elements.container.setAttribute('data-badge-text', label); this.elements.container.setAttribute('data-badge-text', label);
}; };
@@ -212,14 +222,14 @@ class Ads {
this.cuePoints = this.manager.getCuePoints(); this.cuePoints = this.manager.getCuePoints();
// Add advertisement cue's within the time line if available // Add advertisement cue's within the time line if available
if (!utils.is.empty(this.cuePoints)) { if (!is.empty(this.cuePoints)) {
this.cuePoints.forEach(cuePoint => { this.cuePoints.forEach(cuePoint => {
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) { if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
const seekElement = this.player.elements.progress; const seekElement = this.player.elements.progress;
if (utils.is.element(seekElement)) { if (is.element(seekElement)) {
const cuePercentage = 100 / this.player.duration * cuePoint; const cuePercentage = 100 / this.player.duration * cuePoint;
const cue = utils.createElement('span', { const cue = createElement('span', {
class: this.player.config.classNames.cues, class: this.player.config.classNames.cues,
}); });
@@ -266,7 +276,7 @@ class Ads {
// Proxy event // Proxy event
const dispatchEvent = type => { const dispatchEvent = type => {
const event = `ads${type.replace(/_/g, '').toLowerCase()}`; const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
utils.dispatchEvent.call(this.player, this.player.media, event); triggerEvent.call(this.player, this.player.media, event);
}; };
switch (event.type) { switch (event.type) {
@@ -393,7 +403,7 @@ class Ads {
this.player.on('seeked', () => { this.player.on('seeked', () => {
const seekedTime = this.player.currentTime; const seekedTime = this.player.currentTime;
if (utils.is.empty(this.cuePoints)) { if (is.empty(this.cuePoints)) {
return; return;
} }
@@ -530,9 +540,9 @@ class Ads {
trigger(event, ...args) { trigger(event, ...args) {
const handlers = this.events[event]; const handlers = this.events[event];
if (utils.is.array(handlers)) { if (is.array(handlers)) {
handlers.forEach(handler => { handlers.forEach(handler => {
if (utils.is.function(handler)) { if (is.function(handler)) {
handler.apply(this, args); handler.apply(this, args);
} }
}); });
@@ -546,7 +556,7 @@ class Ads {
* @return {Ads} * @return {Ads}
*/ */
on(event, callback) { on(event, callback) {
if (!utils.is.array(this.events[event])) { if (!is.array(this.events[event])) {
this.events[event] = []; this.events[event] = [];
} }
@@ -577,7 +587,7 @@ class Ads {
* @param {string} from * @param {string} from
*/ */
clearSafetyTimer(from) { clearSafetyTimer(from) {
if (!utils.is.nullOrUndefined(this.safetyTimer)) { if (!is.nullOrUndefined(this.safetyTimer)) {
this.player.debug.log(`Safety timer cleared from: ${from}`); this.player.debug.log(`Safety timer cleared from: ${from}`);
clearTimeout(this.safetyTimer); clearTimeout(this.safetyTimer);
+84 -51
View File
@@ -5,28 +5,57 @@
import captions from './../captions'; import captions from './../captions';
import controls from './../controls'; import controls from './../controls';
import ui from './../ui'; import ui from './../ui';
import utils from './../utils'; import { createElement, replaceElement, toggleClass } from './../utils/elements';
import { triggerEvent } from './../utils/events';
import fetch from './../utils/fetch';
import is from './../utils/is';
import loadScript from './../utils/loadScript';
import { format, stripHTML } from './../utils/strings';
import { buildUrlParams } from './../utils/urls';
// Parse Vimeo ID from URL
function parseId(url) {
if (is.empty(url)) {
return null;
}
if (is.number(Number(url))) {
return url;
}
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
return url.match(regex) ? RegExp.$2 : url;
}
// Get aspect ratio for dimensions
function getAspectRatio(width, height) {
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
const ratio = getRatio(width, height);
return `${width / ratio}:${height / ratio}`;
}
// Set playback state and trigger change (only on actual change) // Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) { function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) { if (this.media.paused === play) {
this.media.paused = !play; this.media.paused = !play;
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); triggerEvent.call(this, this.media, play ? 'play' : 'pause');
} }
} }
const vimeo = { const vimeo = {
setup() { setup() {
// Add embed class for responsive // Add embed class for responsive
utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set intial ratio // Set intial ratio
vimeo.setAspectRatio.call(this); vimeo.setAspectRatio.call(this);
// Load the API if not already // Load the API if not already
if (!utils.is.object(window.Vimeo)) { if (!is.object(window.Vimeo)) {
utils loadScript(this.config.urls.vimeo.sdk)
.loadScript(this.config.urls.vimeo.sdk)
.then(() => { .then(() => {
vimeo.ready.call(this); vimeo.ready.call(this);
}) })
@@ -41,8 +70,8 @@ const vimeo = {
// Set aspect ratio // Set aspect ratio
// For Vimeo we have an extra 300% height <div> to hide the standard controls and UI // For Vimeo we have an extra 300% height <div> to hide the standard controls and UI
setAspectRatio(input) { setAspectRatio(input) {
const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':'); const [x, y] = (is.string(input) ? input : this.config.ratio).split(':');
const padding = 100 / ratio[0] * ratio[1]; const padding = 100 / x * y;
this.elements.wrapper.style.paddingBottom = `${padding}%`; this.elements.wrapper.style.paddingBottom = `${padding}%`;
if (this.supported.ui) { if (this.supported.ui) {
@@ -70,34 +99,37 @@ const vimeo = {
gesture: 'media', gesture: 'media',
playsinline: !this.config.fullscreen.iosNative, playsinline: !this.config.fullscreen.iosNative,
}; };
const params = utils.buildUrlParams(options); const params = buildUrlParams(options);
// Get the source URL or ID // Get the source URL or ID
let source = player.media.getAttribute('src'); let source = player.media.getAttribute('src');
// Get from <div> if needed // Get from <div> if needed
if (utils.is.empty(source)) { if (is.empty(source)) {
source = player.media.getAttribute(player.config.attributes.embed.id); source = player.media.getAttribute(player.config.attributes.embed.id);
} }
const id = utils.parseVimeoId(source); const id = parseId(source);
// Build an iframe // Build an iframe
const iframe = utils.createElement('iframe'); const iframe = createElement('iframe');
const src = utils.format(player.config.urls.vimeo.iframe, id, params); const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src); iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', ''); iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', ''); iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay'); iframe.setAttribute('allow', 'autoplay');
// Get poster, if already set
const { poster } = player;
// Inject the package // Inject the package
const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer }); const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe); wrapper.appendChild(iframe);
player.media = utils.replaceElement(wrapper, player.media); player.media = replaceElement(wrapper, player.media);
// Get poster image // Get poster image
utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => { fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (utils.is.empty(response)) { if (is.empty(response)) {
return; return;
} }
@@ -108,7 +140,7 @@ const vimeo = {
url.pathname = `${url.pathname.split('_')[0]}.jpg`; url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// Set and show poster // Set and show poster
ui.setPoster.call(player, url.href); ui.setPoster.call(player, url.href).catch(() => {});
}); });
// Setup instance // Setup instance
@@ -153,19 +185,20 @@ const vimeo = {
// Get current paused state and volume etc // Get current paused state and volume etc
const { embed, media, paused, volume } = player; const { embed, media, paused, volume } = player;
const restorePause = paused && !embed.hasPlayed;
// Set seeking state and trigger event // Set seeking state and trigger event
media.seeking = true; media.seeking = true;
utils.dispatchEvent.call(player, media, 'seeking'); triggerEvent.call(player, media, 'seeking');
// If paused, mute until seek is complete // If paused, mute until seek is complete
Promise.resolve(paused && embed.setVolume(0)) Promise.resolve(restorePause && embed.setVolume(0))
// Seek // Seek
.then(() => embed.setCurrentTime(time)) .then(() => embed.setCurrentTime(time))
// Restore paused // Restore paused
.then(() => paused && embed.pause()) .then(() => restorePause && embed.pause())
// Restore volume // Restore volume
.then(() => paused && embed.setVolume(volume)) .then(() => restorePause && embed.setVolume(volume))
.catch(() => { .catch(() => {
// Do nothing // Do nothing
}); });
@@ -183,7 +216,7 @@ const vimeo = {
.setPlaybackRate(input) .setPlaybackRate(input)
.then(() => { .then(() => {
speed = input; speed = input;
utils.dispatchEvent.call(player, player.media, 'ratechange'); triggerEvent.call(player, player.media, 'ratechange');
}) })
.catch(error => { .catch(error => {
// Hide menu item (and menu if empty) // Hide menu item (and menu if empty)
@@ -203,7 +236,7 @@ const vimeo = {
set(input) { set(input) {
player.embed.setVolume(input).then(() => { player.embed.setVolume(input).then(() => {
volume = input; volume = input;
utils.dispatchEvent.call(player, player.media, 'volumechange'); triggerEvent.call(player, player.media, 'volumechange');
}); });
}, },
}); });
@@ -215,11 +248,11 @@ const vimeo = {
return muted; return muted;
}, },
set(input) { set(input) {
const toggle = utils.is.boolean(input) ? input : false; const toggle = is.boolean(input) ? input : false;
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => { player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
muted = toggle; muted = toggle;
utils.dispatchEvent.call(player, player.media, 'volumechange'); triggerEvent.call(player, player.media, 'volumechange');
}); });
}, },
}); });
@@ -231,7 +264,7 @@ const vimeo = {
return loop; return loop;
}, },
set(input) { set(input) {
const toggle = utils.is.boolean(input) ? input : player.config.loop.active; const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => { player.embed.setLoop(toggle).then(() => {
loop = toggle; loop = toggle;
@@ -264,11 +297,8 @@ const vimeo = {
}); });
// Set aspect ratio based on video size // Set aspect ratio based on video size
Promise.all([ Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
player.embed.getVideoWidth(), const ratio = getAspectRatio(dimensions[0], dimensions[1]);
player.embed.getVideoHeight(),
]).then(dimensions => {
const ratio = utils.getAspectRatio(dimensions[0], dimensions[1]);
vimeo.setAspectRatio.call(this, ratio); vimeo.setAspectRatio.call(this, ratio);
}); });
@@ -286,13 +316,13 @@ const vimeo = {
// Get current time // Get current time
player.embed.getCurrentTime().then(value => { player.embed.getCurrentTime().then(value => {
currentTime = value; currentTime = value;
utils.dispatchEvent.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
}); });
// Get duration // Get duration
player.embed.getDuration().then(value => { player.embed.getDuration().then(value => {
player.media.duration = value; player.media.duration = value;
utils.dispatchEvent.call(player, player.media, 'durationchange'); triggerEvent.call(player, player.media, 'durationchange');
}); });
// Get captions // Get captions
@@ -301,18 +331,21 @@ const vimeo = {
captions.setup.call(player); captions.setup.call(player);
}); });
player.embed.on('cuechange', data => { player.embed.on('cuechange', ({ cues = [] }) => {
let cue = null; const strippedCues = cues.map(cue => stripHTML(cue.text));
captions.updateCues.call(player, strippedCues);
if (data.cues.length) {
cue = utils.stripHTML(data.cues[0].text);
}
captions.setText.call(player, cue);
}); });
player.embed.on('loaded', () => { player.embed.on('loaded', () => {
if (utils.is.element(player.embed.element) && player.supported.ui) { // 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');
}
});
if (is.element(player.embed.element) && player.supported.ui) {
const frame = player.embed.element; const frame = player.embed.element;
// Fix keyboard focus issues // Fix keyboard focus issues
@@ -323,7 +356,7 @@ const vimeo = {
player.embed.on('play', () => { player.embed.on('play', () => {
assurePlaybackState.call(player, true); assurePlaybackState.call(player, true);
utils.dispatchEvent.call(player, player.media, 'playing'); triggerEvent.call(player, player.media, 'playing');
}); });
player.embed.on('pause', () => { player.embed.on('pause', () => {
@@ -333,16 +366,16 @@ const vimeo = {
player.embed.on('timeupdate', data => { player.embed.on('timeupdate', data => {
player.media.seeking = false; player.media.seeking = false;
currentTime = data.seconds; currentTime = data.seconds;
utils.dispatchEvent.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
}); });
player.embed.on('progress', data => { player.embed.on('progress', data => {
player.media.buffered = data.percent; player.media.buffered = data.percent;
utils.dispatchEvent.call(player, player.media, 'progress'); triggerEvent.call(player, player.media, 'progress');
// Check all loaded // Check all loaded
if (parseInt(data.percent, 10) === 1) { if (parseInt(data.percent, 10) === 1) {
utils.dispatchEvent.call(player, player.media, 'canplaythrough'); triggerEvent.call(player, player.media, 'canplaythrough');
} }
// Get duration as if we do it before load, it gives an incorrect value // Get duration as if we do it before load, it gives an incorrect value
@@ -350,24 +383,24 @@ const vimeo = {
player.embed.getDuration().then(value => { player.embed.getDuration().then(value => {
if (value !== player.media.duration) { if (value !== player.media.duration) {
player.media.duration = value; player.media.duration = value;
utils.dispatchEvent.call(player, player.media, 'durationchange'); triggerEvent.call(player, player.media, 'durationchange');
} }
}); });
}); });
player.embed.on('seeked', () => { player.embed.on('seeked', () => {
player.media.seeking = false; player.media.seeking = false;
utils.dispatchEvent.call(player, player.media, 'seeked'); triggerEvent.call(player, player.media, 'seeked');
}); });
player.embed.on('ended', () => { player.embed.on('ended', () => {
player.media.paused = true; player.media.paused = true;
utils.dispatchEvent.call(player, player.media, 'ended'); triggerEvent.call(player, player.media, 'ended');
}); });
player.embed.on('error', detail => { player.embed.on('error', detail => {
player.media.error = detail; player.media.error = detail;
utils.dispatchEvent.call(player, player.media, 'error'); triggerEvent.call(player, player.media, 'error');
}); });
// Rebuild UI // Rebuild UI
+109 -142
View File
@@ -4,88 +4,81 @@
import controls from './../controls'; import controls from './../controls';
import ui from './../ui'; import ui from './../ui';
import utils from './../utils'; import { dedupe } from './../utils/arrays';
import { createElement, replaceElement, toggleClass } from './../utils/elements';
import { triggerEvent } from './../utils/events';
import fetch from './../utils/fetch';
import is from './../utils/is';
import loadImage from './../utils/loadImage';
import loadScript from './../utils/loadScript';
import { format, generateId } from './../utils/strings';
// Parse YouTube ID from URL
function parseId(url) {
if (is.empty(url)) {
return null;
}
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
return url.match(regex) ? RegExp.$2 : url;
}
// Standardise YouTube quality unit // Standardise YouTube quality unit
function mapQualityUnit(input) { function mapQualityUnit(input) {
switch (input) { const qualities = {
case 'hd2160': hd2160: 2160,
return 2160; hd1440: 1440,
hd1080: 1080,
hd720: 720,
large: 480,
medium: 360,
small: 240,
tiny: 144,
};
case 2160: const entry = Object.entries(qualities).find(entry => entry.includes(input));
return 'hd2160';
case 'hd1440': if (entry) {
return 1440; // Get the match corresponding to the input
return entry.find(value => value !== input);
case 1440:
return 'hd1440';
case 'hd1080':
return 1080;
case 1080:
return 'hd1080';
case 'hd720':
return 720;
case 720:
return 'hd720';
case 'large':
return 480;
case 480:
return 'large';
case 'medium':
return 360;
case 360:
return 'medium';
case 'small':
return 240;
case 240:
return 'small';
default:
return 'default';
} }
return 'default';
} }
function mapQualityUnits(levels) { function mapQualityUnits(levels) {
if (utils.is.empty(levels)) { if (is.empty(levels)) {
return levels; return levels;
} }
return utils.dedupe(levels.map(level => mapQualityUnit(level))); return dedupe(levels.map(level => mapQualityUnit(level)));
} }
// Set playback state and trigger change (only on actual change) // Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) { function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) { if (this.media.paused === play) {
this.media.paused = !play; this.media.paused = !play;
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); triggerEvent.call(this, this.media, play ? 'play' : 'pause');
} }
} }
const youtube = { const youtube = {
setup() { setup() {
// Add embed class for responsive // Add embed class for responsive
utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set aspect ratio // Set aspect ratio
youtube.setAspectRatio.call(this); youtube.setAspectRatio.call(this);
// Setup API // Setup API
if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) { if (is.object(window.YT) && is.function(window.YT.Player)) {
youtube.ready.call(this); youtube.ready.call(this);
} else { } else {
// Load the API // Load the API
utils.loadScript(this.config.urls.youtube.sdk).catch(error => { loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error); this.debug.warn('YouTube API failed to load', error);
}); });
@@ -112,10 +105,10 @@ const youtube = {
// Try via undocumented API method first // Try via undocumented API method first
// This method disappears now and then though... // This method disappears now and then though...
// https://github.com/sampotts/plyr/issues/709 // https://github.com/sampotts/plyr/issues/709
if (utils.is.function(this.embed.getVideoData)) { if (is.function(this.embed.getVideoData)) {
const { title } = this.embed.getVideoData(); const { title } = this.embed.getVideoData();
if (utils.is.empty(title)) { if (is.empty(title)) {
this.config.title = title; this.config.title = title;
ui.setTitle.call(this); ui.setTitle.call(this);
return; return;
@@ -124,13 +117,12 @@ const youtube = {
// Or via Google API // Or via Google API
const key = this.config.keys.google; const key = this.config.keys.google;
if (utils.is.string(key) && !utils.is.empty(key)) { if (is.string(key) && !is.empty(key)) {
const url = utils.format(this.config.urls.youtube.api, videoId, key); const url = format(this.config.urls.youtube.api, videoId, key);
utils fetch(url)
.fetch(url)
.then(result => { .then(result => {
if (utils.is.object(result)) { if (is.object(result)) {
this.config.title = result.items[0].snippet.title; this.config.title = result.items[0].snippet.title;
ui.setTitle.call(this); ui.setTitle.call(this);
} }
@@ -151,7 +143,7 @@ const youtube = {
// Ignore already setup (race condition) // Ignore already setup (race condition)
const currentId = player.media.getAttribute('id'); const currentId = player.media.getAttribute('id');
if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) { if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return; return;
} }
@@ -159,30 +151,36 @@ const youtube = {
let source = player.media.getAttribute('src'); let source = player.media.getAttribute('src');
// Get from <div> if needed // Get from <div> if needed
if (utils.is.empty(source)) { if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id); source = player.media.getAttribute(this.config.attributes.embed.id);
} }
// Replace the <iframe> with a <div> due to YouTube API issues // Replace the <iframe> with a <div> due to YouTube API issues
const videoId = utils.parseYouTubeId(source); const videoId = parseId(source);
const id = utils.generateId(player.provider); const id = generateId(player.provider);
const container = utils.createElement('div', { id });
player.media = utils.replaceElement(container, player.media);
// Set poster image // Get poster, if already set
const { poster } = player;
// Replace media element
const container = createElement('div', { id, poster });
player.media = replaceElement(container, player.media);
// Id to poster wrapper
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`; const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide) // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
.catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3 .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
.catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
.then(image => ui.setPoster.call(player, image.src)) .then(image => ui.setPoster.call(player, image.src))
.then(posterSrc => { .then(posterSrc => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters) // If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!posterSrc.includes('maxres')) { if (!posterSrc.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover'; player.elements.poster.style.backgroundSize = 'cover';
} }
}); })
.catch(() => {});
// Setup instance // Setup instance
// https://developers.google.com/youtube/iframe_api_reference // https://developers.google.com/youtube/iframe_api_reference
@@ -208,49 +206,26 @@ const youtube = {
}, },
events: { events: {
onError(event) { onError(event) {
// If we've already fired an error, don't do it again // YouTube may fire onError twice, so only handle it once
// YouTube fires onError twice if (!player.media.error) {
if (utils.is.object(player.media.error)) { const code = event.data;
return; // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
const message =
{
2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
101: 'The owner of the requested video does not allow it to be played in embedded players.',
150: 'The owner of the requested video does not allow it to be played in embedded players.',
}[code] || 'An unknown error occured';
player.media.error = { code, message };
triggerEvent.call(player, player.media, 'error');
} }
const detail = {
code: event.data,
};
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
switch (event.data) {
case 2:
detail.message =
'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.';
break;
case 5:
detail.message =
'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.';
break;
case 100:
detail.message =
'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.';
break;
case 101:
case 150:
detail.message = 'The owner of the requested video does not allow it to be played in embedded players.';
break;
default:
detail.message = 'An unknown error occured';
break;
}
player.media.error = detail;
utils.dispatchEvent.call(player, player.media, 'error');
}, },
onPlaybackQualityChange() { onPlaybackQualityChange() {
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, { triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: player.media.quality, quality: player.media.quality,
}); });
}, },
@@ -261,7 +236,7 @@ const youtube = {
// Get current speed // Get current speed
player.media.playbackRate = instance.getPlaybackRate(); player.media.playbackRate = instance.getPlaybackRate();
utils.dispatchEvent.call(player, player.media, 'ratechange'); triggerEvent.call(player, player.media, 'ratechange');
}, },
onReady(event) { onReady(event) {
// Get the instance // Get the instance
@@ -295,14 +270,14 @@ const youtube = {
return Number(instance.getCurrentTime()); return Number(instance.getCurrentTime());
}, },
set(time) { set(time) {
// If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet). // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
if (player.paused) { if (player.paused && !player.embed.hasPlayed) {
player.embed.mute(); player.embed.mute();
} }
// Set seeking state and trigger event // Set seeking state and trigger event
player.media.seeking = true; player.media.seeking = true;
utils.dispatchEvent.call(player, player.media, 'seeking'); triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent // Seek after events sent
instance.seekTo(time); instance.seekTo(time);
@@ -325,15 +300,7 @@ const youtube = {
return mapQualityUnit(instance.getPlaybackQuality()); return mapQualityUnit(instance.getPlaybackQuality());
}, },
set(input) { set(input) {
const quality = input; instance.setPlaybackQuality(mapQualityUnit(input));
// Set via API
instance.setPlaybackQuality(mapQualityUnit(quality));
// Trigger request event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
quality,
});
}, },
}); });
@@ -346,7 +313,7 @@ const youtube = {
set(input) { set(input) {
volume = input; volume = input;
instance.setVolume(volume * 100); instance.setVolume(volume * 100);
utils.dispatchEvent.call(player, player.media, 'volumechange'); triggerEvent.call(player, player.media, 'volumechange');
}, },
}); });
@@ -357,10 +324,10 @@ const youtube = {
return muted; return muted;
}, },
set(input) { set(input) {
const toggle = utils.is.boolean(input) ? input : muted; const toggle = is.boolean(input) ? input : muted;
muted = toggle; muted = toggle;
instance[toggle ? 'mute' : 'unMute'](); instance[toggle ? 'mute' : 'unMute']();
utils.dispatchEvent.call(player, player.media, 'volumechange'); triggerEvent.call(player, player.media, 'volumechange');
}, },
}); });
@@ -386,8 +353,8 @@ const youtube = {
player.media.setAttribute('tabindex', -1); player.media.setAttribute('tabindex', -1);
} }
utils.dispatchEvent.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
utils.dispatchEvent.call(player, player.media, 'durationchange'); triggerEvent.call(player, player.media, 'durationchange');
// Reset timer // Reset timer
clearInterval(player.timers.buffering); clearInterval(player.timers.buffering);
@@ -399,7 +366,7 @@ const youtube = {
// Trigger progress only when we actually buffer something // Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) { if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
utils.dispatchEvent.call(player, player.media, 'progress'); triggerEvent.call(player, player.media, 'progress');
} }
// Set last buffer point // Set last buffer point
@@ -410,7 +377,7 @@ const youtube = {
clearInterval(player.timers.buffering); clearInterval(player.timers.buffering);
// Trigger event // Trigger event
utils.dispatchEvent.call(player, player.media, 'canplaythrough'); triggerEvent.call(player, player.media, 'canplaythrough');
} }
}, 200); }, 200);
@@ -424,15 +391,12 @@ const youtube = {
// Reset timer // Reset timer
clearInterval(player.timers.playing); clearInterval(player.timers.playing);
const seeked = player.media.seeking && [ const seeked = player.media.seeking && [1, 2].includes(event.data);
1,
2,
].includes(event.data);
if (seeked) { if (seeked) {
// Unset seeking and fire seeked event // Unset seeking and fire seeked event
player.media.seeking = false; player.media.seeking = false;
utils.dispatchEvent.call(player, player.media, 'seeked'); triggerEvent.call(player, player.media, 'seeked');
} }
// Handle events // Handle events
@@ -445,11 +409,11 @@ const youtube = {
switch (event.data) { switch (event.data) {
case -1: case -1:
// Update scrubber // Update scrubber
utils.dispatchEvent.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube // Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction(); player.media.buffered = instance.getVideoLoadedFraction();
utils.dispatchEvent.call(player, player.media, 'progress'); triggerEvent.call(player, player.media, 'progress');
break; break;
@@ -462,23 +426,23 @@ const youtube = {
instance.stopVideo(); instance.stopVideo();
instance.playVideo(); instance.playVideo();
} else { } else {
utils.dispatchEvent.call(player, player.media, 'ended'); triggerEvent.call(player, player.media, 'ended');
} }
break; break;
case 1: case 1:
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet) // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (player.media.paused) { if (player.media.paused && !player.embed.hasPlayed) {
player.media.pause(); player.media.pause();
} else { } else {
assurePlaybackState.call(player, true); assurePlaybackState.call(player, true);
utils.dispatchEvent.call(player, player.media, 'playing'); triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress // Poll to get playback progress
player.timers.playing = setInterval(() => { player.timers.playing = setInterval(() => {
utils.dispatchEvent.call(player, player.media, 'timeupdate'); triggerEvent.call(player, player.media, 'timeupdate');
}, 50); }, 50);
// Check duration again due to YouTube bug // Check duration again due to YouTube bug
@@ -486,11 +450,14 @@ const youtube = {
// https://code.google.com/p/gdata-issues/issues/detail?id=8690 // https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) { if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration(); player.media.duration = instance.getDuration();
utils.dispatchEvent.call(player, player.media, 'durationchange'); triggerEvent.call(player, player.media, 'durationchange');
} }
// Get quality // Get quality
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels())); controls.setQualityMenu.call(
player,
mapQualityUnits(instance.getAvailableQualityLevels()),
);
} }
break; break;
@@ -508,7 +475,7 @@ const youtube = {
break; break;
} }
utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, { triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data, code: event.data,
}); });
}, },
+150 -206
View File
@@ -1,14 +1,15 @@
// ========================================================================== // ==========================================================================
// Plyr // Plyr
// plyr.js v3.3.10 // plyr.js v3.3.19
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================
import captions from './captions'; import captions from './captions';
import defaults from './config/defaults';
import { getProviderByUrl, providers, types } from './config/types';
import Console from './console'; import Console from './console';
import controls from './controls'; import controls from './controls';
import defaults from './defaults';
import Fullscreen from './fullscreen'; import Fullscreen from './fullscreen';
import Listeners from './listeners'; import Listeners from './listeners';
import media from './media'; import media from './media';
@@ -16,9 +17,14 @@ import Ads from './plugins/ads';
import source from './source'; import source from './source';
import Storage from './storage'; import Storage from './storage';
import support from './support'; import support from './support';
import { providers, types } from './types';
import ui from './ui'; import ui from './ui';
import utils from './utils'; import { closest } from './utils/arrays';
import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';
import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
import is from './utils/is';
import loadSprite from './utils/loadSprite';
import { cloneDeep, extend } from './utils/objects';
import { parseUrl } from './utils/urls';
// Private properties // Private properties
// TODO: Use a WeakMap for private globals // TODO: Use a WeakMap for private globals
@@ -41,18 +47,18 @@ class Plyr {
this.media = target; this.media = target;
// String selector passed // String selector passed
if (utils.is.string(this.media)) { if (is.string(this.media)) {
this.media = document.querySelectorAll(this.media); this.media = document.querySelectorAll(this.media);
} }
// jQuery, NodeList or Array passed, use first element // jQuery, NodeList or Array passed, use first element
if ((window.jQuery && this.media instanceof jQuery) || utils.is.nodeList(this.media) || utils.is.array(this.media)) { if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) {
// eslint-disable-next-line // eslint-disable-next-line
this.media = this.media[0]; this.media = this.media[0];
} }
// Set config // Set config
this.config = utils.extend( this.config = extend(
{}, {},
defaults, defaults,
Plyr.defaults, Plyr.defaults,
@@ -84,7 +90,8 @@ class Plyr {
// Captions // Captions
this.captions = { this.captions = {
active: null, active: null,
currentTrack: null, currentTrack: -1,
meta: new WeakMap(),
}; };
// Fullscreen // Fullscreen
@@ -96,7 +103,6 @@ class Plyr {
this.options = { this.options = {
speed: [], speed: [],
quality: [], quality: [],
captions: [],
}; };
// Debugging // Debugging
@@ -108,7 +114,7 @@ class Plyr {
this.debug.log('Support', support); this.debug.log('Support', support);
// We need an element to setup // We need an element to setup
if (utils.is.nullOrUndefined(this.media) || !utils.is.element(this.media)) { if (is.nullOrUndefined(this.media) || !is.element(this.media)) {
this.debug.error('Setup failed: no suitable element passed'); this.debug.error('Setup failed: no suitable element passed');
return; return;
} }
@@ -144,7 +150,6 @@ class Plyr {
// Embed properties // Embed properties
let iframe = null; let iframe = null;
let url = null; let url = null;
let params = null;
// Different setup based on type // Different setup based on type
switch (type) { switch (type) {
@@ -153,10 +158,10 @@ class Plyr {
iframe = this.media.querySelector('iframe'); iframe = this.media.querySelector('iframe');
// <iframe> type // <iframe> type
if (utils.is.element(iframe)) { if (is.element(iframe)) {
// Detect provider // Detect provider
url = iframe.getAttribute('src'); url = parseUrl(iframe.getAttribute('src'));
this.provider = utils.getProviderByUrl(url); this.provider = getProviderByUrl(url.toString());
// Rework elements // Rework elements
this.elements.container = this.media; this.elements.container = this.media;
@@ -166,24 +171,20 @@ class Plyr {
this.elements.container.className = ''; this.elements.container.className = '';
// Get attributes from URL and set config // Get attributes from URL and set config
params = utils.getUrlParams(url); if (url.searchParams.length) {
if (!utils.is.empty(params)) { const truthy = ['1', 'true'];
const truthy = [
'1',
'true',
];
if (truthy.includes(params.autoplay)) { if (truthy.includes(url.searchParams.get('autoplay'))) {
this.config.autoplay = true; this.config.autoplay = true;
} }
if (truthy.includes(params.loop)) { if (truthy.includes(url.searchParams.get('loop'))) {
this.config.loop.active = true; this.config.loop.active = true;
} }
// TODO: replace fullscreen.iosNative with this playsinline config option // TODO: replace fullscreen.iosNative with this playsinline config option
// YouTube requires the playsinline in the URL // YouTube requires the playsinline in the URL
if (this.isYouTube) { if (this.isYouTube) {
this.config.playsinline = truthy.includes(params.playsinline); this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));
} else { } else {
this.config.playsinline = true; this.config.playsinline = true;
} }
@@ -197,7 +198,7 @@ class Plyr {
} }
// Unsupported or missing provider // Unsupported or missing provider
if (utils.is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) { if (is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
this.debug.error('Setup failed: Invalid provider'); this.debug.error('Setup failed: Invalid provider');
return; return;
} }
@@ -245,6 +246,8 @@ class Plyr {
return; return;
} }
this.eventListeners = [];
// Create listeners // Create listeners
this.listeners = new Listeners(this); this.listeners = new Listeners(this);
@@ -255,14 +258,11 @@ class Plyr {
this.media.plyr = this; this.media.plyr = this;
// Wrap media // Wrap media
if (!utils.is.element(this.elements.container)) { if (!is.element(this.elements.container)) {
this.elements.container = utils.createElement('div'); this.elements.container = createElement('div');
utils.wrap(this.media, this.elements.container); wrap(this.media, this.elements.container);
} }
// Allow focus to be captured
this.elements.container.setAttribute('tabindex', 0);
// Add style hook // Add style hook
ui.addStyleHook.call(this); ui.addStyleHook.call(this);
@@ -271,7 +271,7 @@ class Plyr {
// Listen for events if debugging // Listen for events if debugging
if (this.config.debug) { if (this.config.debug) {
utils.on(this.elements.container, this.config.events.join(' '), event => { on.call(this, this.elements.container, this.config.events.join(' '), event => {
this.debug.log(`event: ${event.type}`); this.debug.log(`event: ${event.type}`);
}); });
} }
@@ -330,7 +330,7 @@ class Plyr {
* Play the media, or play the advertisement (if they are not blocked) * Play the media, or play the advertisement (if they are not blocked)
*/ */
play() { play() {
if (!utils.is.function(this.media.play)) { if (!is.function(this.media.play)) {
return null; return null;
} }
@@ -342,7 +342,7 @@ class Plyr {
* Pause the media * Pause the media
*/ */
pause() { pause() {
if (!this.playing || !utils.is.function(this.media.pause)) { if (!this.playing || !is.function(this.media.pause)) {
return; return;
} }
@@ -383,7 +383,7 @@ class Plyr {
*/ */
togglePlay(input) { togglePlay(input) {
// Toggle based on current state if nothing passed // Toggle based on current state if nothing passed
const toggle = utils.is.boolean(input) ? input : !this.playing; const toggle = is.boolean(input) ? input : !this.playing;
if (toggle) { if (toggle) {
this.play(); this.play();
@@ -399,7 +399,7 @@ class Plyr {
if (this.isHTML5) { if (this.isHTML5) {
this.pause(); this.pause();
this.restart(); this.restart();
} else if (utils.is.function(this.media.stop)) { } else if (is.function(this.media.stop)) {
this.media.stop(); this.media.stop();
} }
} }
@@ -416,7 +416,7 @@ class Plyr {
* @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
*/ */
rewind(seekTime) { rewind(seekTime) {
this.currentTime = this.currentTime - (utils.is.number(seekTime) ? seekTime : this.config.seekTime); this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime);
} }
/** /**
@@ -424,7 +424,7 @@ class Plyr {
* @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime * @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
*/ */
forward(seekTime) { forward(seekTime) {
this.currentTime = this.currentTime + (utils.is.number(seekTime) ? seekTime : this.config.seekTime); this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime);
} }
/** /**
@@ -438,7 +438,7 @@ class Plyr {
} }
// Validate input // Validate input
const inputIsValid = utils.is.number(input) && input > 0; const inputIsValid = is.number(input) && input > 0;
// Set // Set
this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0; this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
@@ -461,7 +461,7 @@ class Plyr {
const { buffered } = this.media; const { buffered } = this.media;
// YouTube / Vimeo return a float between 0-1 // YouTube / Vimeo return a float between 0-1
if (utils.is.number(buffered)) { if (is.number(buffered)) {
return buffered; return buffered;
} }
@@ -505,17 +505,17 @@ class Plyr {
const max = 1; const max = 1;
const min = 0; const min = 0;
if (utils.is.string(volume)) { if (is.string(volume)) {
volume = Number(volume); volume = Number(volume);
} }
// Load volume from storage if no value specified // Load volume from storage if no value specified
if (!utils.is.number(volume)) { if (!is.number(volume)) {
volume = this.storage.get('volume'); volume = this.storage.get('volume');
} }
// Use config if all else fails // Use config if all else fails
if (!utils.is.number(volume)) { if (!is.number(volume)) {
({ volume } = this.config); ({ volume } = this.config);
} }
@@ -535,7 +535,7 @@ class Plyr {
this.media.volume = volume; this.media.volume = volume;
// If muted, and we're increasing volume manually, reset muted state // If muted, and we're increasing volume manually, reset muted state
if (!utils.is.empty(value) && this.muted && volume > 0) { if (!is.empty(value) && this.muted && volume > 0) {
this.muted = false; this.muted = false;
} }
} }
@@ -553,7 +553,7 @@ class Plyr {
*/ */
increaseVolume(step) { increaseVolume(step) {
const volume = this.media.muted ? 0 : this.volume; const volume = this.media.muted ? 0 : this.volume;
this.volume = volume + (utils.is.number(step) ? step : 1); this.volume = volume + (is.number(step) ? step : 0);
} }
/** /**
@@ -561,8 +561,7 @@ class Plyr {
* @param {boolean} step - How much to decrease by (between 0 and 1) * @param {boolean} step - How much to decrease by (between 0 and 1)
*/ */
decreaseVolume(step) { decreaseVolume(step) {
const volume = this.media.muted ? 0 : this.volume; this.increaseVolume(-step);
this.volume = volume - (utils.is.number(step) ? step : 1);
} }
/** /**
@@ -573,12 +572,12 @@ class Plyr {
let toggle = mute; let toggle = mute;
// Load muted state from storage // Load muted state from storage
if (!utils.is.boolean(toggle)) { if (!is.boolean(toggle)) {
toggle = this.storage.get('muted'); toggle = this.storage.get('muted');
} }
// Use config if all else fails // Use config if all else fails
if (!utils.is.boolean(toggle)) { if (!is.boolean(toggle)) {
toggle = this.config.muted; toggle = this.config.muted;
} }
@@ -624,15 +623,15 @@ class Plyr {
set speed(input) { set speed(input) {
let speed = null; let speed = null;
if (utils.is.number(input)) { if (is.number(input)) {
speed = input; speed = input;
} }
if (!utils.is.number(speed)) { if (!is.number(speed)) {
speed = this.storage.get('speed'); speed = this.storage.get('speed');
} }
if (!utils.is.number(speed)) { if (!is.number(speed)) {
speed = this.config.speed.selected; speed = this.config.speed.selected;
} }
@@ -669,36 +668,31 @@ class Plyr {
* @param {number} input - Quality level * @param {number} input - Quality level
*/ */
set quality(input) { set quality(input) {
let quality = null; const config = this.config.quality;
const options = this.options.quality;
if (!utils.is.empty(input)) { if (!options.length) {
quality = Number(input);
}
if (!utils.is.number(quality)) {
quality = this.storage.get('quality');
}
if (!utils.is.number(quality)) {
quality = this.config.quality.selected;
}
if (!utils.is.number(quality)) {
quality = this.config.quality.default;
}
if (!this.options.quality.length) {
return; return;
} }
if (!this.options.quality.includes(quality)) { let quality = [
const closest = utils.closest(this.options.quality, quality); !is.empty(input) && Number(input),
this.debug.warn(`Unsupported quality option: ${quality}, using ${closest} instead`); this.storage.get('quality'),
quality = closest; config.selected,
config.default,
].find(is.number);
if (!options.includes(quality)) {
const value = closest(options, quality);
this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);
quality = value;
} }
// Trigger request event
triggerEvent.call(this, this.media, 'qualityrequested', false, { quality });
// Update config // Update config
this.config.quality.selected = quality; config.selected = quality;
// Set quality // Set quality
this.media.quality = quality; this.media.quality = quality;
@@ -717,7 +711,7 @@ class Plyr {
* @param {boolean} input - Whether to loop or not * @param {boolean} input - Whether to loop or not
*/ */
set loop(input) { set loop(input) {
const toggle = utils.is.boolean(input) ? input : this.config.loop.active; const toggle = is.boolean(input) ? input : this.config.loop.active;
this.config.loop.active = toggle; this.config.loop.active = toggle;
this.media.loop = toggle; this.media.loop = toggle;
@@ -797,7 +791,7 @@ class Plyr {
return; return;
} }
ui.setPoster.call(this, input); ui.setPoster.call(this, input, false).catch(() => {});
} }
/** /**
@@ -816,7 +810,7 @@ class Plyr {
* @param {boolean} input - Whether to autoplay or not * @param {boolean} input - Whether to autoplay or not
*/ */
set autoplay(input) { set autoplay(input) {
const toggle = utils.is.boolean(input) ? input : this.config.autoplay; const toggle = is.boolean(input) ? input : this.config.autoplay;
this.config.autoplay = toggle; this.config.autoplay = toggle;
} }
@@ -832,83 +826,39 @@ class Plyr {
* @param {boolean} input - Whether to enable captions * @param {boolean} input - Whether to enable captions
*/ */
toggleCaptions(input) { toggleCaptions(input) {
// If there's no full support captions.toggle.call(this, input, false);
if (!this.supported.ui) {
return;
}
// If the method is called without parameter, toggle based on current value
const active = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
// Toggle state
utils.toggleState(this.elements.buttons.captions, active);
// Add class hook
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, active);
// Update state and trigger event
if (active !== this.captions.active) {
this.captions.active = active;
utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
}
} }
/** /**
* Set the captions language * Set the caption track by index
* @param {number} - Caption index
*/
set currentTrack(input) {
captions.set.call(this, input, false);
}
/**
* Get the current caption track index (-1 if disabled)
*/
get currentTrack() {
const { toggled, currentTrack } = this.captions;
return toggled ? currentTrack : -1;
}
/**
* Set the wanted language for captions
* Since tracks can be added later it won't update the actual caption track until there is a matching track
* @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc) * @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
*/ */
set language(input) { set language(input) {
// Nothing specified captions.setLanguage.call(this, input, false);
if (!utils.is.string(input)) {
return;
}
// If empty string is passed, assume disable captions
if (utils.is.empty(input)) {
this.toggleCaptions(false);
return;
}
// Normalize
const language = input.toLowerCase();
// Check for support
if (!this.options.captions.includes(language)) {
this.debug.warn(`Unsupported language option: ${language}`);
return;
}
// Ensure captions are enabled
this.toggleCaptions(true);
// Enabled only
if (language === 'enabled') {
return;
}
// If nothing to change, bail
if (this.language === language) {
return;
}
// Update config
this.captions.language = language;
// Clear caption
captions.setText.call(this, null);
// Update captions
captions.setLanguage.call(this);
// Trigger an event
utils.dispatchEvent.call(this, this.media, 'languagechange');
} }
/** /**
* Get the current captions language * Get the current track's language
*/ */
get language() { get language() {
return this.captions.language; return (captions.getCurrentTrack.call(this) || {}).language;
} }
/** /**
@@ -928,7 +878,7 @@ class Plyr {
} }
// Toggle based on current state if not passed // Toggle based on current state if not passed
const toggle = utils.is.boolean(input) ? input : this.pip === states.inline; const toggle = is.boolean(input) ? input : this.pip === states.inline;
// Toggle based on current state // Toggle based on current state
this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline); this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline);
@@ -964,22 +914,22 @@ class Plyr {
// Don't toggle if missing UI support or if it's audio // Don't toggle if missing UI support or if it's audio
if (this.supported.ui && !this.isAudio) { if (this.supported.ui && !this.isAudio) {
// Get state before change // Get state before change
const isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls); const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);
// Negate the argument if not undefined since adding the class to hides the controls // Negate the argument if not undefined since adding the class to hides the controls
const force = typeof toggle === 'undefined' ? undefined : !toggle; const force = typeof toggle === 'undefined' ? undefined : !toggle;
// Apply and get updated state // Apply and get updated state
const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force); const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);
// Close menu // Close menu
if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) { if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
controls.toggleMenu.call(this, false); controls.toggleMenu.call(this, false);
} }
// Trigger event on change // Trigger event on change
if (hiding !== isHidden) { if (hiding !== isHidden) {
const eventName = hiding ? 'controlshidden' : 'controlsshown'; const eventName = hiding ? 'controlshidden' : 'controlsshown';
utils.dispatchEvent.call(this, this.media, eventName); triggerEvent.call(this, this.media, eventName);
} }
return !hiding; return !hiding;
} }
@@ -992,16 +942,23 @@ class Plyr {
* @param {function} callback - Callback for when event occurs * @param {function} callback - Callback for when event occurs
*/ */
on(event, callback) { on(event, callback) {
utils.on(this.elements.container, event, callback); on.call(this, this.elements.container, event, callback);
}
/**
* Add event listeners once
* @param {string} event - Event type
* @param {function} callback - Callback for when event occurs
*/
once(event, callback) {
once.call(this, this.elements.container, event, callback);
} }
/** /**
* Remove event listeners * Remove event listeners
* @param {string} event - Event type * @param {string} event - Event type
* @param {function} callback - Callback for when event occurs * @param {function} callback - Callback for when event occurs
*/ */
off(event, callback) { off(event, callback) {
utils.off(this.elements.container, event, callback); off(this.elements.container, event, callback);
} }
/** /**
@@ -1027,10 +984,10 @@ class Plyr {
if (soft) { if (soft) {
if (Object.keys(this.elements).length) { if (Object.keys(this.elements).length) {
// Remove elements // Remove elements
utils.removeElement(this.elements.buttons.play); removeElement(this.elements.buttons.play);
utils.removeElement(this.elements.captions); removeElement(this.elements.captions);
utils.removeElement(this.elements.controls); removeElement(this.elements.controls);
utils.removeElement(this.elements.wrapper); removeElement(this.elements.wrapper);
// Clear for GC // Clear for GC
this.elements.buttons.play = null; this.elements.buttons.play = null;
@@ -1040,21 +997,21 @@ class Plyr {
} }
// Callback // Callback
if (utils.is.function(callback)) { if (is.function(callback)) {
callback(); callback();
} }
} else { } else {
// Unbind listeners // Unbind listeners
this.listeners.clear(); unbindListeners.call(this);
// Replace the container with the original element provided // Replace the container with the original element provided
utils.replaceElement(this.elements.original, this.elements.container); replaceElement(this.elements.original, this.elements.container);
// Event // Event
utils.dispatchEvent.call(this, this.elements.original, 'destroyed', true); triggerEvent.call(this, this.elements.original, 'destroyed', true);
// Callback // Callback
if (utils.is.function(callback)) { if (is.function(callback)) {
callback.call(this.elements.original); callback.call(this.elements.original);
} }
@@ -1072,50 +1029,37 @@ class Plyr {
// Stop playback // Stop playback
this.stop(); this.stop();
// Type specific stuff // Provider specific stuff
switch (`${this.provider}:${this.type}`) { if (this.isHTML5) {
case 'html5:video': // Clear timeout
case 'html5:audio': clearTimeout(this.timers.loading);
// Clear timeout
clearTimeout(this.timers.loading);
// Restore native video controls // Restore native video controls
ui.toggleNativeControls.call(this, true); ui.toggleNativeControls.call(this, true);
// Clean up // Clean up
done(); done();
} else if (this.isYouTube) {
// Clear timers
clearInterval(this.timers.buffering);
clearInterval(this.timers.playing);
break; // Destroy YouTube API
if (this.embed !== null && is.function(this.embed.destroy)) {
this.embed.destroy();
}
case 'youtube:video': // Clean up
// Clear timers done();
clearInterval(this.timers.buffering); } else if (this.isVimeo) {
clearInterval(this.timers.playing); // Destroy Vimeo API
// then clean up (wait, to prevent postmessage errors)
if (this.embed !== null) {
this.embed.unload().then(done);
}
// Destroy YouTube API // Vimeo does not always return
if (this.embed !== null && utils.is.function(this.embed.destroy)) { setTimeout(done, 200);
this.embed.destroy();
}
// Clean up
done();
break;
case 'vimeo:video':
// Destroy Vimeo API
// then clean up (wait, to prevent postmessage errors)
if (this.embed !== null) {
this.embed.unload().then(done);
}
// Vimeo does not always return
setTimeout(done, 200);
break;
default:
break;
} }
} }
@@ -1143,7 +1087,7 @@ class Plyr {
* @param {string} [id] - Unique ID * @param {string} [id] - Unique ID
*/ */
static loadSprite(url, id) { static loadSprite(url, id) {
return utils.loadSprite(url, id); return loadSprite(url, id);
} }
/** /**
@@ -1154,15 +1098,15 @@ class Plyr {
static setup(selector, options = {}) { static setup(selector, options = {}) {
let targets = null; let targets = null;
if (utils.is.string(selector)) { if (is.string(selector)) {
targets = Array.from(document.querySelectorAll(selector)); targets = Array.from(document.querySelectorAll(selector));
} else if (utils.is.nodeList(selector)) { } else if (is.nodeList(selector)) {
targets = Array.from(selector); targets = Array.from(selector);
} else if (utils.is.array(selector)) { } else if (is.array(selector)) {
targets = selector.filter(i => utils.is.element(i)); targets = selector.filter(is.element);
} }
if (utils.is.empty(targets)) { if (is.empty(targets)) {
return null; return null;
} }
@@ -1170,6 +1114,6 @@ class Plyr {
} }
} }
Plyr.defaults = utils.cloneDeep(defaults); Plyr.defaults = cloneDeep(defaults);
export default Plyr; export default Plyr;
+1 -1
View File
@@ -1,6 +1,6 @@
// ========================================================================== // ==========================================================================
// Plyr Polyfilled Build // Plyr Polyfilled Build
// plyr.js v3.3.10 // plyr.js v3.3.19
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================
+26 -37
View File
@@ -2,23 +2,25 @@
// Plyr source update // Plyr source update
// ========================================================================== // ==========================================================================
import { providers } from './config/types';
import html5 from './html5'; import html5 from './html5';
import media from './media'; import media from './media';
import support from './support'; import support from './support';
import { providers } from './types';
import ui from './ui'; import ui from './ui';
import utils from './utils'; import { createElement, insertElement, removeElement } from './utils/elements';
import is from './utils/is';
import { getDeep } from './utils/objects';
const source = { const source = {
// Add elements to HTML5 media (source, tracks, etc) // Add elements to HTML5 media (source, tracks, etc)
insertElements(type, attributes) { insertElements(type, attributes) {
if (utils.is.string(attributes)) { if (is.string(attributes)) {
utils.insertElement(type, this.media, { insertElement(type, this.media, {
src: attributes, src: attributes,
}); });
} else if (utils.is.array(attributes)) { } else if (is.array(attributes)) {
attributes.forEach(attribute => { attributes.forEach(attribute => {
utils.insertElement(type, this.media, attribute); insertElement(type, this.media, attribute);
}); });
} }
}, },
@@ -26,7 +28,7 @@ const source = {
// Update source // Update source
// Sources are not checked for support so be careful // Sources are not checked for support so be careful
change(input) { change(input) {
if (!utils.is.object(input) || !('sources' in input) || !input.sources.length) { if (!getDeep(input, 'sources.length')) {
this.debug.warn('Invalid source format'); this.debug.warn('Invalid source format');
return; return;
} }
@@ -42,47 +44,34 @@ const source = {
this.options.quality = []; this.options.quality = [];
// Remove elements // Remove elements
utils.removeElement(this.media); removeElement(this.media);
this.media = null; this.media = null;
// Reset class name // Reset class name
if (utils.is.element(this.elements.container)) { if (is.element(this.elements.container)) {
this.elements.container.removeAttribute('class'); this.elements.container.removeAttribute('class');
} }
// Set the type and provider // Set the type and provider
this.type = input.type; const { sources, type } = input;
this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5; const [{ provider = providers.html5, src }] = sources;
const tagName = provider === 'html5' ? type : 'div';
const attributes = provider === 'html5' ? {} : { src };
// Check for support Object.assign(this, {
this.supported = support.check(this.type, this.provider, this.config.playsinline); provider,
type,
// Create new markup // Check for support
switch (`${this.provider}:${this.type}`) { supported: support.check(type, provider, this.config.playsinline),
case 'html5:video': // Create new element
this.media = utils.createElement('video'); media: createElement(tagName, attributes),
break; });
case 'html5:audio':
this.media = utils.createElement('audio');
break;
case 'youtube:video':
case 'vimeo:video':
this.media = utils.createElement('div', {
src: input.sources[0].src,
});
break;
default:
break;
}
// Inject the new element // Inject the new element
this.elements.container.appendChild(this.media); this.elements.container.appendChild(this.media);
// Autoplay the new source? // Autoplay the new source?
if (utils.is.boolean(input.autoplay)) { if (is.boolean(input.autoplay)) {
this.config.autoplay = input.autoplay; this.config.autoplay = input.autoplay;
} }
@@ -94,7 +83,7 @@ const source = {
if (this.config.autoplay) { if (this.config.autoplay) {
this.media.setAttribute('autoplay', ''); this.media.setAttribute('autoplay', '');
} }
if (!utils.is.empty(input.poster)) { if (!is.empty(input.poster)) {
this.poster = input.poster; this.poster = input.poster;
} }
if (this.config.loop.active) { if (this.config.loop.active) {
@@ -113,7 +102,7 @@ const source = {
// Set new sources for html5 // Set new sources for html5
if (this.isHTML5) { if (this.isHTML5) {
source.insertElements.call(this, 'source', input.sources); source.insertElements.call(this, 'source', sources);
} }
// Set video title // Set video title
+7 -6
View File
@@ -2,7 +2,8 @@
// Plyr storage // Plyr storage
// ========================================================================== // ==========================================================================
import utils from './utils'; import is from './utils/is';
import { extend } from './utils/objects';
class Storage { class Storage {
constructor(player) { constructor(player) {
@@ -37,13 +38,13 @@ class Storage {
const store = window.localStorage.getItem(this.key); const store = window.localStorage.getItem(this.key);
if (utils.is.empty(store)) { if (is.empty(store)) {
return null; return null;
} }
const json = JSON.parse(store); const json = JSON.parse(store);
return utils.is.string(key) && key.length ? json[key] : json; return is.string(key) && key.length ? json[key] : json;
} }
set(object) { set(object) {
@@ -53,7 +54,7 @@ class Storage {
} }
// Can only store objectst // Can only store objectst
if (!utils.is.object(object)) { if (!is.object(object)) {
return; return;
} }
@@ -61,12 +62,12 @@ class Storage {
let storage = this.get(); let storage = this.get();
// Default to empty object // Default to empty object
if (utils.is.empty(storage)) { if (is.empty(storage)) {
storage = {}; storage = {};
} }
// Update the working copy of the values // Update the working copy of the values
utils.extend(storage, object); extend(storage, object);
// Update storage // Update storage
window.localStorage.setItem(this.key, JSON.stringify(storage)); window.localStorage.setItem(this.key, JSON.stringify(storage));
+38 -101
View File
@@ -2,7 +2,19 @@
// Plyr support checks // Plyr support checks
// ========================================================================== // ==========================================================================
import utils from './utils'; import { transitionEndEvent } from './utils/animation';
import browser from './utils/browser';
import { createElement } from './utils/elements';
import is from './utils/is';
// Default codecs for checking mimetype support
const defaultCodecs = {
'audio/ogg': 'vorbis',
'audio/wav': '1',
'video/webm': 'vp8, vorbis',
'video/mp4': 'avc1.42E01E, mp4a.40.2',
'video/ogg': 'theora',
};
// Check for feature support // Check for feature support
const support = { const support = {
@@ -13,32 +25,9 @@ const support = {
// Check for support // Check for support
// Basic functionality vs full UI // Basic functionality vs full UI
check(type, provider, playsinline) { check(type, provider, playsinline) {
let api = false;
let ui = false;
const browser = utils.getBrowser();
const canPlayInline = browser.isIPhone && playsinline && support.playsinline; const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
const api = support[type] || provider !== 'html5';
switch (`${provider}:${type}`) { const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
case 'html5:video':
api = support.video;
ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline);
break;
case 'html5:audio':
api = support.audio;
ui = api && support.rangeInput;
break;
case 'youtube:video':
case 'vimeo:video':
api = true;
ui = support.rangeInput && (!browser.isIPhone || canPlayInline);
break;
default:
api = support.audio && support.video;
ui = api && support.rangeInput;
}
return { return {
api, api,
@@ -48,14 +37,11 @@ const support = {
// Picture-in-picture support // Picture-in-picture support
// Safari only currently // Safari only currently
pip: (() => { pip: (() => !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode))(),
const browser = utils.getBrowser();
return !browser.isIPhone && utils.is.function(utils.createElement('video').webkitSetPresentationMode);
})(),
// Airplay support // Airplay support
// Safari only currently // Safari only currently
airplay: utils.is.function(window.WebKitPlaybackTargetAvailabilityEvent), airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
// Inline playback support // Inline playback support
// https://webkit.org/blog/6784/new-video-policies-for-ios/ // https://webkit.org/blog/6784/new-video-policies-for-ios/
@@ -64,83 +50,34 @@ const support = {
// Check for mime type support against a player instance // Check for mime type support against a player instance
// Credits: http://diveintohtml5.info/everything.html // Credits: http://diveintohtml5.info/everything.html
// Related: http://www.leanbackplayer.com/test/h5mt.html // Related: http://www.leanbackplayer.com/test/h5mt.html
mime(type) { mime(inputType) {
const { media } = this; const [mediaType] = inputType.split('/');
if (!this.isHTML5 || mediaType !== this.type) {
try {
// Bail if no checking function
if (!this.isHTML5 || !utils.is.function(media.canPlayType)) {
return false;
}
// Check directly if codecs specified
if (type.includes('codecs=')) {
return media.canPlayType(type).replace(/no/, '');
}
// Type specific checks
if (this.isVideo) {
switch (type) {
case 'video/webm':
return media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, '');
case 'video/mp4':
return media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, '');
case 'video/ogg':
return media.canPlayType('video/ogg; codecs="theora"').replace(/no/, '');
default:
return false;
}
} else if (this.isAudio) {
switch (type) {
case 'audio/mpeg':
return media.canPlayType('audio/mpeg;').replace(/no/, '');
case 'audio/ogg':
return media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, '');
case 'audio/wav':
return media.canPlayType('audio/wav; codecs="1"').replace(/no/, '');
default:
return false;
}
}
} catch (e) {
return false; return false;
} }
// If we got this far, we're stuffed let type;
return false; 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]}"`;
}
try {
return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
} catch (err) {
return false;
}
}, },
// Check for textTracks support // Check for textTracks support
textTracks: 'textTracks' in document.createElement('video'), textTracks: 'textTracks' in document.createElement('video'),
// Check for passive event listener support
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
// https://www.youtube.com/watch?v=NPM6172J22g
passiveListeners: (() => {
// Test via a getter in the options object to see if the passive property is accessed
let supported = false;
try {
const options = Object.defineProperty({}, 'passive', {
get() {
supported = true;
return null;
},
});
window.addEventListener('test', null, options);
window.removeEventListener('test', null, options);
} catch (e) {
// Do nothing
}
return supported;
})(),
// <input type="range"> Sliders // <input type="range"> Sliders
rangeInput: (() => { rangeInput: (() => {
const range = document.createElement('input'); const range = document.createElement('input');
@@ -153,7 +90,7 @@ const support = {
touch: 'ontouchstart' in document.documentElement, touch: 'ontouchstart' in document.documentElement,
// Detect transitions support // Detect transitions support
transitions: utils.transitionEndEvent !== false, transitions: transitionEndEvent !== false,
// Reduced motion iOS & MacOS setting // Reduced motion iOS & MacOS setting
// https://webkit.org/blog/7551/responsive-design-for-motion/ // https://webkit.org/blog/7551/responsive-design-for-motion/
+74 -62
View File
@@ -6,15 +6,16 @@ import captions from './captions';
import controls from './controls'; import controls from './controls';
import i18n from './i18n'; import i18n from './i18n';
import support from './support'; import support from './support';
import utils from './utils'; import browser from './utils/browser';
import { getElement, toggleClass } from './utils/elements';
// Sniff out the browser import { ready, triggerEvent } from './utils/events';
const browser = utils.getBrowser(); import is from './utils/is';
import loadImage from './utils/loadImage';
const ui = { const ui = {
addStyleHook() { addStyleHook() {
utils.toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
utils.toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui); toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
}, },
// Toggle native HTML5 media controls // Toggle native HTML5 media controls
@@ -44,7 +45,7 @@ const ui = {
} }
// Inject custom controls if not present // Inject custom controls if not present
if (!utils.is.element(this.elements.controls)) { if (!is.element(this.elements.controls)) {
// Inject custom controls // Inject custom controls
controls.inject.call(this); controls.inject.call(this);
@@ -85,31 +86,35 @@ const ui = {
ui.checkPlaying.call(this); ui.checkPlaying.call(this);
// Check for picture-in-picture support // Check for picture-in-picture support
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo); toggleClass(
this.elements.container,
this.config.classNames.pip.supported,
support.pip && this.isHTML5 && this.isVideo,
);
// Check for airplay support // Check for airplay support
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class // Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class // Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
// Ready for API calls // Ready for API calls
this.ready = true; this.ready = true;
// Ready event at end of execution stack // Ready event at end of execution stack
setTimeout(() => { setTimeout(() => {
utils.dispatchEvent.call(this, this.media, 'ready'); triggerEvent.call(this, this.media, 'ready');
}, 0); }, 0);
// Set the title // Set the title
ui.setTitle.call(this); ui.setTitle.call(this);
// Assure the poster image is set, if the property was added before the element was created // Assure the poster image is set, if the property was added before the element was created
if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) { if (this.poster) {
ui.setPoster.call(this, this.poster); ui.setPoster.call(this, this.poster, false).catch(() => {});
} }
// Manually set the duration if user has overridden it. // Manually set the duration if user has overridden it.
@@ -125,31 +130,26 @@ const ui = {
let label = i18n.get('play', this.config); let label = i18n.get('play', this.config);
// If there's a media title set, use that for the label // If there's a media title set, use that for the label
if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) { if (is.string(this.config.title) && !is.empty(this.config.title)) {
label += `, ${this.config.title}`; label += `, ${this.config.title}`;
// Set container label
this.elements.container.setAttribute('aria-label', this.config.title);
} }
// If there's a play button, set label // If there's a play button, set label
if (utils.is.nodeList(this.elements.buttons.play)) { Array.from(this.elements.buttons.play || []).forEach(button => {
Array.from(this.elements.buttons.play).forEach(button => { button.setAttribute('aria-label', label);
button.setAttribute('aria-label', label); });
});
}
// Set iframe title // Set iframe title
// https://github.com/sampotts/plyr/issues/124 // https://github.com/sampotts/plyr/issues/124
if (this.isEmbed) { if (this.isEmbed) {
const iframe = utils.getElement.call(this, 'iframe'); const iframe = getElement.call(this, 'iframe');
if (!utils.is.element(iframe)) { if (!is.element(iframe)) {
return; return;
} }
// Default to media type // Default to media type
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video'; const title = !is.empty(this.config.title) ? this.config.title : 'video';
const format = i18n.get('frameTitle', this.config); const format = i18n.get('frameTitle', this.config);
iframe.setAttribute('title', format.replace('{title}', title)); iframe.setAttribute('title', format.replace('{title}', title));
@@ -158,51 +158,66 @@ const ui = {
// Toggle poster // Toggle poster
togglePoster(enable) { togglePoster(enable) {
utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
}, },
// Set the poster image (async) // Set the poster image (async)
setPoster(poster) { // Used internally for the poster setter, with the passive option forced to false
// Set property regardless of validity setPoster(poster, passive = true) {
this.media.setAttribute('poster', poster); // Don't override if call is passive
if (passive && this.poster) {
// Bail if element is missing return Promise.reject(new Error('Poster already set'));
if (!utils.is.element(this.elements.poster)) {
return Promise.reject();
} }
// Load the image, and set poster if successful // Set property synchronously to respect the call order
const loadPromise = utils.loadImage(poster) this.media.setAttribute('poster', poster);
.then(() => {
this.elements.poster.style.backgroundImage = `url('${poster}')`;
Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`,
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
ui.togglePoster.call(this, true);
return poster;
});
// Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video) // Wait until ui is ready
loadPromise.catch(() => ui.togglePoster.call(this, false)); return (
ready
// Return the promise so the caller can use it as well .call(this)
return loadPromise; // Load image
.then(() => loadImage(poster))
.catch(err => {
// Hide poster on error unless it's been set by another call
if (poster === this.poster) {
ui.togglePoster.call(this, false);
}
// Rethrow
throw err;
})
.then(() => {
// Prevent race conditions
if (poster !== this.poster) {
throw new Error('setPoster cancelled by later call to setPoster');
}
})
.then(() => {
Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`,
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
ui.togglePoster.call(this, true);
return poster;
})
);
}, },
// Check playing state // Check playing state
checkPlaying(event) { checkPlaying(event) {
// Class hooks // Class hooks
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing); toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused); toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set ARIA state // Set state
utils.toggleState(this.elements.buttons.play, this.playing); Array.from(this.elements.buttons.play || []).forEach(target => {
target.pressed = this.playing;
});
// Only update controls on non timeupdate events // Only update controls on non timeupdate events
if (utils.is.event(event) && event.type === 'timeupdate') { if (is.event(event) && event.type === 'timeupdate') {
return; return;
} }
@@ -212,10 +227,7 @@ const ui = {
// Check if media is loading // Check if media is loading
checkLoading(event) { checkLoading(event) {
this.loading = [ this.loading = ['stalled', 'waiting'].includes(event.type);
'stalled',
'waiting',
].includes(event.type);
// Clear timer // Clear timer
clearTimeout(this.timers.loading); clearTimeout(this.timers.loading);
@@ -223,7 +235,7 @@ const ui = {
// Timer to prevent flicker when seeking // Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => { this.timers.loading = setTimeout(() => {
// Update progress bar loading class state // Update progress bar loading class state
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading); toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Update controls visibility // Update controls visibility
ui.toggleControls.call(this); ui.toggleControls.call(this);
-869
View File
@@ -1,869 +0,0 @@
// ==========================================================================
// Plyr utils
// ==========================================================================
import loadjs from 'loadjs';
import Storage from './storage';
import support from './support';
import { providers } from './types';
const utils = {
// Check variable types
is: {
object(input) {
return this.getConstructor(input) === Object;
},
number(input) {
return this.getConstructor(input) === Number && !Number.isNaN(input);
},
string(input) {
return this.getConstructor(input) === String;
},
boolean(input) {
return this.getConstructor(input) === Boolean;
},
function(input) {
return this.getConstructor(input) === Function;
},
array(input) {
return !this.nullOrUndefined(input) && Array.isArray(input);
},
weakMap(input) {
return this.instanceof(input, WeakMap);
},
nodeList(input) {
return this.instanceof(input, NodeList);
},
element(input) {
return this.instanceof(input, Element);
},
textNode(input) {
return this.getConstructor(input) === Text;
},
event(input) {
return this.instanceof(input, Event);
},
cue(input) {
return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue);
},
track(input) {
return this.instanceof(input, TextTrack) || (!this.nullOrUndefined(input) && this.string(input.kind));
},
url(input) {
return !this.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input);
},
nullOrUndefined(input) {
return input === null || typeof input === 'undefined';
},
empty(input) {
return (
this.nullOrUndefined(input) ||
((this.string(input) || this.array(input) || this.nodeList(input)) && !input.length) ||
(this.object(input) && !Object.keys(input).length)
);
},
instanceof(input, constructor) {
return Boolean(input && constructor && input instanceof constructor);
},
getConstructor(input) {
return !this.nullOrUndefined(input) ? input.constructor : null;
},
},
// Unfortunately, due to mixed support, UA sniffing is required
getBrowser() {
return {
isIE: /* @cc_on!@ */ false || !!document.documentMode,
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform),
};
},
// Fetch wrapper
// Using XHR to avoid issues with older browsers
fetch(url, responseType = 'text') {
return new Promise((resolve, reject) => {
try {
const request = new XMLHttpRequest();
// Check for CORS support
if (!('withCredentials' in request)) {
return;
}
request.addEventListener('load', () => {
if (responseType === 'text') {
try {
resolve(JSON.parse(request.responseText));
} catch (e) {
resolve(request.responseText);
}
} else {
resolve(request.response);
}
});
request.addEventListener('error', () => {
throw new Error(request.statusText);
});
request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send();
} catch (e) {
reject(e);
}
});
},
// Load image avoiding xhr/fetch CORS issues
// Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded.
// By default it checks if it is at least 1px, but you can add a second argument to change this.
loadImage(src, minWidth = 1) {
return new Promise((resolve, reject) => {
const image = new Image();
const handler = () => {
delete image.onload;
delete image.onerror;
(image.naturalWidth >= minWidth ? resolve : reject)(image);
};
Object.assign(image, {onload: handler, onerror: handler, src});
});
},
// Load an external script
loadScript(url) {
return new Promise((resolve, reject) => {
loadjs(url, {
success: resolve,
error: reject,
});
});
},
// Load an external SVG sprite
loadSprite(url, id) {
if (!utils.is.string(url)) {
return;
}
const prefix = 'cache-';
const hasId = utils.is.string(id);
let isCached = false;
const exists = () => document.querySelectorAll(`#${id}`).length;
function injectSprite(data) {
// Check again incase of race condition
if (hasId && exists()) {
return;
}
// Inject content
this.innerHTML = data;
// Inject the SVG to the body
document.body.insertBefore(this, document.body.childNodes[0]);
}
// Only load once if ID set
if (!hasId || !exists()) {
const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
utils.toggleHidden(container, true);
if (hasId) {
container.setAttribute('id', id);
}
// Check in cache
if (useStorage) {
const cached = window.localStorage.getItem(prefix + id);
isCached = cached !== null;
if (isCached) {
const data = JSON.parse(cached);
injectSprite.call(container, data.content);
return;
}
}
// Get the sprite
utils
.fetch(url)
.then(result => {
if (utils.is.empty(result)) {
return;
}
if (useStorage) {
window.localStorage.setItem(
prefix + id,
JSON.stringify({
content: result,
}),
);
}
injectSprite.call(container, result);
})
.catch(() => {});
}
},
// Generate a random ID
generateId(prefix) {
return `${prefix}-${Math.floor(Math.random() * 10000)}`;
},
// Wrap an element
wrap(elements, wrapper) {
// Convert `elements` to an array, if necessary.
const targets = elements.length ? elements : [elements];
// Loops backwards to prevent having to clone the wrapper on the
// first element (see `child` below).
Array.from(targets)
.reverse()
.forEach((element, index) => {
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
// Cache the current parent and sibling.
const parent = element.parentNode;
const sibling = element.nextSibling;
// Wrap the element (is automatically removed from its current
// parent).
child.appendChild(element);
// If the element had a sibling, insert the wrapper before
// the sibling to maintain the HTML structure; otherwise, just
// append it to the parent.
if (sibling) {
parent.insertBefore(child, sibling);
} else {
parent.appendChild(child);
}
});
},
// Create a DocumentFragment
createElement(type, attributes, text) {
// Create a new <element>
const element = document.createElement(type);
// Set all passed attributes
if (utils.is.object(attributes)) {
utils.setAttributes(element, attributes);
}
// Add text node
if (utils.is.string(text)) {
element.innerText = text;
}
// Return built element
return element;
},
// Inaert an element after another
insertAfter(element, target) {
target.parentNode.insertBefore(element, target.nextSibling);
},
// Insert a DocumentFragment
insertElement(type, parent, attributes, text) {
// Inject the new <element>
parent.appendChild(utils.createElement(type, attributes, text));
},
// Remove element(s)
removeElement(element) {
if (utils.is.nodeList(element) || utils.is.array(element)) {
Array.from(element).forEach(utils.removeElement);
return;
}
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
return;
}
element.parentNode.removeChild(element);
},
// Remove all child elements
emptyElement(element) {
let { length } = element.childNodes;
while (length > 0) {
element.removeChild(element.lastChild);
length -= 1;
}
},
// Replace element
replaceElement(newChild, oldChild) {
if (!utils.is.element(oldChild) || !utils.is.element(oldChild.parentNode) || !utils.is.element(newChild)) {
return null;
}
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
},
// Set attributes
setAttributes(element, attributes) {
if (!utils.is.element(element) || utils.is.empty(attributes)) {
return;
}
Object.entries(attributes).forEach(([
key,
value,
]) => {
element.setAttribute(key, value);
});
},
// Get an attribute object from a string selector
getAttributesFromSelector(sel, existingAttributes) {
// For example:
// '.test' to { class: 'test' }
// '#test' to { id: 'test' }
// '[data-test="test"]' to { 'data-test': 'test' }
if (!utils.is.string(sel) || utils.is.empty(sel)) {
return {};
}
const attributes = {};
const existing = existingAttributes;
sel.split(',').forEach(s => {
// Remove whitespace
const selector = s.trim();
const className = selector.replace('.', '');
const stripped = selector.replace(/[[\]]/g, '');
// Get the parts and value
const parts = stripped.split('=');
const key = parts[0];
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
// Get the first character
const start = selector.charAt(0);
switch (start) {
case '.':
// Add to existing classname
if (utils.is.object(existing) && utils.is.string(existing.class)) {
existing.class += ` ${className}`;
}
attributes.class = className;
break;
case '#':
// ID selector
attributes.id = selector.replace('#', '');
break;
case '[':
// Attribute selector
attributes[key] = value;
break;
default:
break;
}
});
return attributes;
},
// Toggle hidden
toggleHidden(element, hidden) {
if (!utils.is.element(element)) {
return;
}
let hide = hidden;
if (!utils.is.boolean(hide)) {
hide = !element.hasAttribute('hidden');
}
if (hide) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
},
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
toggleClass(element, className, force) {
if (utils.is.element(element)) {
let method = 'toggle';
if (typeof force !== 'undefined') {
method = force ? 'add' : 'remove';
}
element.classList[method](className);
return element.classList.contains(className);
}
return null;
},
// Has class name
hasClass(element, className) {
return utils.is.element(element) && element.classList.contains(className);
},
// Element matches selector
matches(element, selector) {
const prototype = { Element };
function match() {
return Array.from(document.querySelectorAll(selector)).includes(this);
}
const matches = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;
return matches.call(element, selector);
},
// Find all elements
getElements(selector) {
return this.elements.container.querySelectorAll(selector);
},
// Find a single element
getElement(selector) {
return this.elements.container.querySelector(selector);
},
// Get the focused element
getFocusElement() {
let focused = document.activeElement;
if (!focused || focused === document.body) {
focused = null;
} else {
focused = document.querySelector(':focus');
}
return focused;
},
// Trap focus inside container
trapFocus(element = null, toggle = false) {
if (!utils.is.element(element)) {
return;
}
const focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
const first = focusable[0];
const last = focusable[focusable.length - 1];
const trap = event => {
// Bail if not tab key or not fullscreen
if (event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element
const focused = utils.getFocusElement();
if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used
first.focus();
event.preventDefault();
} else if (focused === first && event.shiftKey) {
// Move focus to last element that can be tabbed if Shift is used
last.focus();
event.preventDefault();
}
};
if (toggle) {
utils.on(this.elements.container, 'keydown', trap, false);
} else {
utils.off(this.elements.container, 'keydown', trap, false);
}
},
// Toggle event listener
toggleListener(elements, event, callback, toggle = false, passive = true, capture = false) {
// Bail if no elemetns, event, or callback
if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) {
return;
}
// If a nodelist is passed, call itself on each node
if (utils.is.nodeList(elements) || utils.is.array(elements)) {
// Create listener for each node
Array.from(elements).forEach(element => {
if (element instanceof Node) {
utils.toggleListener.call(null, element, event, callback, toggle, passive, capture);
}
});
return;
}
// Allow multiple events
const events = event.split(' ');
// Build options
// Default to just the capture boolean for browsers with no passive listener support
let options = capture;
// If passive events listeners are supported
if (support.passiveListeners) {
options = {
// Whether the listener can be passive (i.e. default never prevented)
passive,
// Whether the listener is a capturing listener or not
capture,
};
}
// If a single node is passed, bind the event listener
events.forEach(type => {
elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
});
},
// Bind event handler
on(element, events = '', callback, passive = true, capture = false) {
utils.toggleListener(element, events, callback, true, passive, capture);
},
// Unbind event handler
off(element, events = '', callback, passive = true, capture = false) {
utils.toggleListener(element, events, callback, false, passive, capture);
},
// Trigger event
dispatchEvent(element, type = '', bubbles = false, detail = {}) {
// Bail if no element
if (!utils.is.element(element) || utils.is.empty(type)) {
return;
}
// Create and dispatch the event
const event = new CustomEvent(type, {
bubbles,
detail: Object.assign({}, detail, {
plyr: this,
}),
});
// Dispatch the event
element.dispatchEvent(event);
},
// Toggle aria-pressed state on a toggle button
// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
toggleState(element, input) {
// If multiple elements passed
if (utils.is.array(element) || utils.is.nodeList(element)) {
Array.from(element).forEach(target => utils.toggleState(target, input));
return;
}
// Bail if no target
if (!utils.is.element(element)) {
return;
}
// Get state
const pressed = element.getAttribute('aria-pressed') === 'true';
const state = utils.is.boolean(input) ? input : !pressed;
// Set the attribute on target
element.setAttribute('aria-pressed', state);
},
// Format string
format(input, ...args) {
if (utils.is.empty(input)) {
return input;
}
return input.toString().replace(/{(\d+)}/g, (match, i) => (utils.is.string(args[i]) ? args[i] : ''));
},
// Get percentage
getPercentage(current, max) {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
return 0;
}
return (current / max * 100).toFixed(2);
},
// Time helpers
getHours(value) {
return parseInt((value / 60 / 60) % 60, 10);
},
getMinutes(value) {
return parseInt((value / 60) % 60, 10);
},
getSeconds(value) {
return parseInt(value % 60, 10);
},
// Format time to UI friendly string
formatTime(time = 0, displayHours = false, inverted = false) {
// Bail if the value isn't a number
if (!utils.is.number(time)) {
return this.formatTime(null, displayHours, inverted);
}
// Format time component to add leading zero
const format = value => `0${value}`.slice(-2);
// Breakdown to hours, mins, secs
let hours = this.getHours(time);
const mins = this.getMinutes(time);
const secs = this.getSeconds(time);
// Do we need to display hours?
if (displayHours || hours > 0) {
hours = `${hours}:`;
} else {
hours = '';
}
// Render
return `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
},
// Replace all occurances of a string in a string
replaceAll(input = '', find = '', replace = '') {
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
},
// Convert to title case
toTitleCase(input = '') {
return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
},
// Convert string to pascalCase
toPascalCase(input = '') {
let string = input.toString();
// Convert kebab case
string = utils.replaceAll(string, '-', ' ');
// Convert snake case
string = utils.replaceAll(string, '_', ' ');
// Convert to title case
string = utils.toTitleCase(string);
// Convert to pascal case
return utils.replaceAll(string, ' ', '');
},
// Convert string to pascalCase
toCamelCase(input = '') {
let string = input.toString();
// Convert to pascal case
string = utils.toPascalCase(string);
// Convert first character to lowercase
return string.charAt(0).toLowerCase() + string.slice(1);
},
// Deep extend destination object with N more objects
extend(target = {}, ...sources) {
if (!sources.length) {
return target;
}
const source = sources.shift();
if (!utils.is.object(source)) {
return target;
}
Object.keys(source).forEach(key => {
if (utils.is.object(source[key])) {
if (!Object.keys(target).includes(key)) {
Object.assign(target, { [key]: {} });
}
utils.extend(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
});
return utils.extend(target, ...sources);
},
// Remove duplicates in an array
dedupe(array) {
if (!utils.is.array(array)) {
return array;
}
return array.filter((item, index) => array.indexOf(item) === index);
},
// Clone nested objects
cloneDeep(object) {
return JSON.parse(JSON.stringify(object));
},
// Get a nested value in an object
getDeep(object, path) {
return path.split('.').reduce((obj, key) => obj && obj[key], object);
},
// Get the closest value in an array
closest(array, value) {
if (!utils.is.array(array) || !array.length) {
return null;
}
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
},
// Get the provider for a given URL
getProviderByUrl(url) {
// YouTube
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) {
return providers.youtube;
}
// Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo;
}
return null;
},
// Parse YouTube ID from URL
parseYouTubeId(url) {
if (utils.is.empty(url)) {
return null;
}
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
return url.match(regex) ? RegExp.$2 : url;
},
// Parse Vimeo ID from URL
parseVimeoId(url) {
if (utils.is.empty(url)) {
return null;
}
if (utils.is.number(Number(url))) {
return url;
}
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
return url.match(regex) ? RegExp.$2 : url;
},
// Convert a URL to a location object
parseUrl(url) {
const parser = document.createElement('a');
parser.href = url;
return parser;
},
// Get URL query parameters
getUrlParams(input) {
let search = input;
// Parse URL if needed
if (input.startsWith('http://') || input.startsWith('https://')) {
({ search } = this.parseUrl(input));
}
if (this.is.empty(search)) {
return null;
}
const hashes = search.slice(search.indexOf('?') + 1).split('&');
return hashes.reduce((params, hash) => {
const [
key,
val,
] = hash.split('=');
return Object.assign(params, { [key]: decodeURIComponent(val) });
}, {});
},
// Convert object to URL parameters
buildUrlParams(input) {
if (!utils.is.object(input)) {
return '';
}
return Object.keys(input)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(input[key])}`)
.join('&');
},
// Remove HTML from a string
stripHTML(source) {
const fragment = document.createDocumentFragment();
const element = document.createElement('div');
fragment.appendChild(element);
element.innerHTML = source;
return fragment.firstChild.innerText;
},
// Get aspect ratio for dimensions
getAspectRatio(width, height) {
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
const ratio = getRatio(width, height);
return `${width / ratio}:${height / ratio}`;
},
// Get the transition end event
get transitionEndEvent() {
const element = document.createElement('span');
const events = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend',
};
const type = Object.keys(events).find(event => element.style[event] !== undefined);
return utils.is.string(type) ? events[type] : false;
},
// Force repaint of element
repaint(element) {
setTimeout(() => {
utils.toggleHidden(element, true);
element.offsetHeight; // eslint-disable-line
utils.toggleHidden(element, false);
}, 0);
},
};
export default utils;
+30
View File
@@ -0,0 +1,30 @@
// ==========================================================================
// Animation utils
// ==========================================================================
import { toggleHidden } from './elements';
import is from './is';
export const transitionEndEvent = (() => {
const element = document.createElement('span');
const events = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend',
};
const type = Object.keys(events).find(event => element.style[event] !== undefined);
return is.string(type) ? events[type] : false;
})();
// Force repaint of element
export function repaint(element) {
setTimeout(() => {
toggleHidden(element, true);
element.offsetHeight; // eslint-disable-line
toggleHidden(element, false);
}, 0);
}
+23
View File
@@ -0,0 +1,23 @@
// ==========================================================================
// Array utils
// ==========================================================================
import is from './is';
// Remove duplicates in an array
export function dedupe(array) {
if (!is.array(array)) {
return array;
}
return array.filter((item, index) => array.indexOf(item) === index);
}
// Get the closest value in an array
export function closest(array, value) {
if (!is.array(array) || !array.length) {
return null;
}
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
}
+13
View File
@@ -0,0 +1,13 @@
// ==========================================================================
// Browser sniffing
// Unfortunately, due to mixed support, UA sniffing is required
// ==========================================================================
const browser = {
isIE: /* @cc_on!@ */ false || !!document.documentMode,
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform),
};
export default browser;
+285
View File
@@ -0,0 +1,285 @@
// ==========================================================================
// Element utils
// ==========================================================================
import { toggleListener } from './events';
import is from './is';
// Wrap an element
export function wrap(elements, wrapper) {
// Convert `elements` to an array, if necessary.
const targets = elements.length ? elements : [elements];
// Loops backwards to prevent having to clone the wrapper on the
// first element (see `child` below).
Array.from(targets)
.reverse()
.forEach((element, index) => {
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
// Cache the current parent and sibling.
const parent = element.parentNode;
const sibling = element.nextSibling;
// Wrap the element (is automatically removed from its current
// parent).
child.appendChild(element);
// If the element had a sibling, insert the wrapper before
// the sibling to maintain the HTML structure; otherwise, just
// append it to the parent.
if (sibling) {
parent.insertBefore(child, sibling);
} else {
parent.appendChild(child);
}
});
}
// Set attributes
export function setAttributes(element, attributes) {
if (!is.element(element) || is.empty(attributes)) {
return;
}
// Assume null and undefined attributes should be left out,
// Setting them would otherwise convert them to "null" and "undefined"
Object.entries(attributes)
.filter(([, value]) => !is.nullOrUndefined(value))
.forEach(([key, value]) => element.setAttribute(key, value));
}
// Create a DocumentFragment
export function createElement(type, attributes, text) {
// Create a new <element>
const element = document.createElement(type);
// Set all passed attributes
if (is.object(attributes)) {
setAttributes(element, attributes);
}
// Add text node
if (is.string(text)) {
element.innerText = text;
}
// Return built element
return element;
}
// Inaert an element after another
export function insertAfter(element, target) {
target.parentNode.insertBefore(element, target.nextSibling);
}
// Insert a DocumentFragment
export function insertElement(type, parent, attributes, text) {
// Inject the new <element>
parent.appendChild(createElement(type, attributes, text));
}
// Remove element(s)
export function removeElement(element) {
if (is.nodeList(element) || is.array(element)) {
Array.from(element).forEach(removeElement);
return;
}
if (!is.element(element) || !is.element(element.parentNode)) {
return;
}
element.parentNode.removeChild(element);
}
// Remove all child elements
export function emptyElement(element) {
let { length } = element.childNodes;
while (length > 0) {
element.removeChild(element.lastChild);
length -= 1;
}
}
// Replace element
export function replaceElement(newChild, oldChild) {
if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) {
return null;
}
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
}
// Get an attribute object from a string selector
export function getAttributesFromSelector(sel, existingAttributes) {
// For example:
// '.test' to { class: 'test' }
// '#test' to { id: 'test' }
// '[data-test="test"]' to { 'data-test': 'test' }
if (!is.string(sel) || is.empty(sel)) {
return {};
}
const attributes = {};
const existing = existingAttributes;
sel.split(',').forEach(s => {
// Remove whitespace
const selector = s.trim();
const className = selector.replace('.', '');
const stripped = selector.replace(/[[\]]/g, '');
// Get the parts and value
const parts = stripped.split('=');
const key = parts[0];
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
// Get the first character
const start = selector.charAt(0);
switch (start) {
case '.':
// Add to existing classname
if (is.object(existing) && is.string(existing.class)) {
existing.class += ` ${className}`;
}
attributes.class = className;
break;
case '#':
// ID selector
attributes.id = selector.replace('#', '');
break;
case '[':
// Attribute selector
attributes[key] = value;
break;
default:
break;
}
});
return attributes;
}
// Toggle hidden
export function toggleHidden(element, hidden) {
if (!is.element(element)) {
return;
}
let hide = hidden;
if (!is.boolean(hide)) {
hide = !element.hasAttribute('hidden');
}
if (hide) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
}
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
export function toggleClass(element, className, force) {
if (is.element(element)) {
let method = 'toggle';
if (typeof force !== 'undefined') {
method = force ? 'add' : 'remove';
}
element.classList[method](className);
return element.classList.contains(className);
}
return null;
}
// Has class name
export function hasClass(element, className) {
return is.element(element) && element.classList.contains(className);
}
// Element matches selector
export function matches(element, selector) {
const prototype = { Element };
function match() {
return Array.from(document.querySelectorAll(selector)).includes(this);
}
const matches =
prototype.matches ||
prototype.webkitMatchesSelector ||
prototype.mozMatchesSelector ||
prototype.msMatchesSelector ||
match;
return matches.call(element, selector);
}
// Find all elements
export function getElements(selector) {
return this.elements.container.querySelectorAll(selector);
}
// Find a single element
export function getElement(selector) {
return this.elements.container.querySelector(selector);
}
// Get the focused element
export function getFocusElement() {
let focused = document.activeElement;
if (!focused || focused === document.body) {
focused = null;
} else {
focused = document.querySelector(':focus');
}
return focused;
}
// Trap focus inside container
export function trapFocus(element = null, toggle = false) {
if (!is.element(element)) {
return;
}
const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
const first = focusable[0];
const last = focusable[focusable.length - 1];
const trap = event => {
// Bail if not tab key or not fullscreen
if (event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element
const focused = getFocusElement();
if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used
first.focus();
event.preventDefault();
} else if (focused === first && event.shiftKey) {
// Move focus to last element that can be tabbed if Shift is used
last.focus();
event.preventDefault();
}
};
toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
}
+120
View File
@@ -0,0 +1,120 @@
// ==========================================================================
// Event utils
// ==========================================================================
import is from './is';
// Check for passive event listener support
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
// https://www.youtube.com/watch?v=NPM6172J22g
const supportsPassiveListeners = (() => {
// Test via a getter in the options object to see if the passive property is accessed
let supported = false;
try {
const options = Object.defineProperty({}, 'passive', {
get() {
supported = true;
return null;
},
});
window.addEventListener('test', null, options);
window.removeEventListener('test', null, options);
} catch (e) {
// Do nothing
}
return supported;
})();
// Toggle event listener
export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {
// Bail if no element, event, or callback
if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {
return;
}
// Allow multiple events
const events = event.split(' ');
// Build options
// Default to just the capture boolean for browsers with no passive listener support
let options = capture;
// If passive events listeners are supported
if (supportsPassiveListeners) {
options = {
// Whether the listener can be passive (i.e. default never prevented)
passive,
// Whether the listener is a capturing listener or not
capture,
};
}
// If a single node is passed, bind the event listener
events.forEach(type => {
if (this && this.eventListeners && toggle) {
// Cache event listener
this.eventListeners.push({ element, type, callback, options });
}
element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
});
}
// Bind event handler
export function on(element, events = '', callback, passive = true, capture = false) {
toggleListener.call(this, element, events, callback, true, passive, capture);
}
// Unbind event handler
export function off(element, events = '', callback, passive = true, capture = false) {
toggleListener.call(this, element, events, callback, false, passive, capture);
}
// Bind once-only event handler
export function once(element, events = '', callback, passive = true, capture = false) {
function onceCallback(...args) {
off(element, events, onceCallback, passive, capture);
callback.apply(this, args);
}
toggleListener.call(this, element, events, onceCallback, true, passive, capture);
}
// Trigger event
export function triggerEvent(element, type = '', bubbles = false, detail = {}) {
// Bail if no element
if (!is.element(element) || is.empty(type)) {
return;
}
// Create and dispatch the event
const event = new CustomEvent(type, {
bubbles,
detail: Object.assign({}, detail, {
plyr: this,
}),
});
// Dispatch the event
element.dispatchEvent(event);
}
// Unbind all cached event listeners
export function unbindListeners() {
if (this && this.eventListeners) {
this.eventListeners.forEach(item => {
const { element, type, callback, options } = item;
element.removeEventListener(type, callback, options);
});
this.eventListeners = [];
}
}
// Run method when / if player is ready
export function ready() {
return new Promise(
resolve => (this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)),
).then(() => {});
}
+42
View File
@@ -0,0 +1,42 @@
// ==========================================================================
// Fetch wrapper
// Using XHR to avoid issues with older browsers
// ==========================================================================
export default function fetch(url, responseType = 'text') {
return new Promise((resolve, reject) => {
try {
const request = new XMLHttpRequest();
// Check for CORS support
if (!('withCredentials' in request)) {
return;
}
request.addEventListener('load', () => {
if (responseType === 'text') {
try {
resolve(JSON.parse(request.responseText));
} catch (e) {
resolve(request.responseText);
}
} else {
resolve(request.response);
}
});
request.addEventListener('error', () => {
throw new Error(request.status);
});
request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send();
} catch (e) {
reject(e);
}
});
}
+63
View File
@@ -0,0 +1,63 @@
// ==========================================================================
// Type checking utils
// ==========================================================================
const getConstructor = input => (input !== null && typeof input !== 'undefined' ? input.constructor : null);
const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);
const isNullOrUndefined = input => input === null || typeof input === 'undefined';
const isObject = input => getConstructor(input) === Object;
const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);
const isString = input => getConstructor(input) === String;
const isBoolean = input => getConstructor(input) === Boolean;
const isFunction = input => getConstructor(input) === Function;
const isArray = input => Array.isArray(input);
const isWeakMap = input => instanceOf(input, WeakMap);
const isNodeList = input => instanceOf(input, NodeList);
const isElement = input => instanceOf(input, Element);
const isTextNode = input => getConstructor(input) === Text;
const isEvent = input => instanceOf(input, Event);
const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));
const isEmpty = input =>
isNullOrUndefined(input) ||
((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||
(isObject(input) && !Object.keys(input).length);
const isUrl = input => {
// Accept a URL object
if (instanceOf(input, window.URL)) {
return true;
}
// Add the protocol if required
let string = input;
if (!input.startsWith('http://') || !input.startsWith('https://')) {
string = `http://${input}`;
}
try {
return !isEmpty(new URL(string).hostname);
} catch (e) {
return false;
}
};
export default {
nullOrUndefined: isNullOrUndefined,
object: isObject,
number: isNumber,
string: isString,
boolean: isBoolean,
function: isFunction,
array: isArray,
weakMap: isWeakMap,
nodeList: isNodeList,
element: isElement,
textNode: isTextNode,
event: isEvent,
cue: isCue,
track: isTrack,
url: isUrl,
empty: isEmpty,
};
+19
View File
@@ -0,0 +1,19 @@
// ==========================================================================
// Load image avoiding xhr/fetch CORS issues
// Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded
// By default it checks if it is at least 1px, but you can add a second argument to change this
// ==========================================================================
export default function loadImage(src, minWidth = 1) {
return new Promise((resolve, reject) => {
const image = new Image();
const handler = () => {
delete image.onload;
delete image.onerror;
(image.naturalWidth >= minWidth ? resolve : reject)(image);
};
Object.assign(image, { onload: handler, onerror: handler, src });
});
}
+14
View File
@@ -0,0 +1,14 @@
// ==========================================================================
// Load an external script
// ==========================================================================
import loadjs from 'loadjs';
export default function loadScript(url) {
return new Promise((resolve, reject) => {
loadjs(url, {
success: resolve,
error: reject,
});
});
}
+76
View File
@@ -0,0 +1,76 @@
// ==========================================================================
// Sprite loader
// ==========================================================================
import Storage from './../storage';
import fetch from './fetch';
import is from './is';
// Load an external SVG sprite
export default function loadSprite(url, id) {
if (!is.string(url)) {
return;
}
const prefix = 'cache';
const hasId = is.string(id);
let isCached = false;
const exists = () => document.getElementById(id) !== null;
const update = (container, data) => {
container.innerHTML = data;
// Check again incase of race condition
if (hasId && exists()) {
return;
}
// Inject the SVG to the body
document.body.insertAdjacentElement('afterbegin', container);
};
// Only load once if ID set
if (!hasId || !exists()) {
const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
container.setAttribute('hidden', '');
if (hasId) {
container.setAttribute('id', id);
}
// Check in cache
if (useStorage) {
const cached = window.localStorage.getItem(`${prefix}-${id}`);
isCached = cached !== null;
if (isCached) {
const data = JSON.parse(cached);
update(container, data.content);
}
}
// Get the sprite
fetch(url)
.then(result => {
if (is.empty(result)) {
return;
}
if (useStorage) {
window.localStorage.setItem(
`${prefix}-${id}`,
JSON.stringify({
content: result,
}),
);
}
update(container, result);
})
.catch(() => {});
}
}
+42
View File
@@ -0,0 +1,42 @@
// ==========================================================================
// Object utils
// ==========================================================================
import is from './is';
// Clone nested objects
export function cloneDeep(object) {
return JSON.parse(JSON.stringify(object));
}
// Get a nested value in an object
export function getDeep(object, path) {
return path.split('.').reduce((obj, key) => obj && obj[key], object);
}
// Deep extend destination object with N more objects
export function extend(target = {}, ...sources) {
if (!sources.length) {
return target;
}
const source = sources.shift();
if (!is.object(source)) {
return target;
}
Object.keys(source).forEach(key => {
if (is.object(source[key])) {
if (!Object.keys(target).includes(key)) {
Object.assign(target, { [key]: {} });
}
extend(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
});
return extend(target, ...sources);
}
+85
View File
@@ -0,0 +1,85 @@
// ==========================================================================
// String utils
// ==========================================================================
import is from './is';
// Generate a random ID
export function generateId(prefix) {
return `${prefix}-${Math.floor(Math.random() * 10000)}`;
}
// Format string
export function format(input, ...args) {
if (is.empty(input)) {
return input;
}
return input.toString().replace(/{(\d+)}/g, (match, i) => args[i].toString());
}
// Get percentage
export function getPercentage(current, max) {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
return 0;
}
return (current / max * 100).toFixed(2);
}
// Replace all occurances of a string in a string
export function replaceAll(input = '', find = '', replace = '') {
return input.replace(
new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'),
replace.toString(),
);
}
// Convert to title case
export function toTitleCase(input = '') {
return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
}
// Convert string to pascalCase
export function toPascalCase(input = '') {
let string = input.toString();
// Convert kebab case
string = replaceAll(string, '-', ' ');
// Convert snake case
string = replaceAll(string, '_', ' ');
// Convert to title case
string = toTitleCase(string);
// Convert to pascal case
return replaceAll(string, ' ', '');
}
// Convert string to pascalCase
export function toCamelCase(input = '') {
let string = input.toString();
// Convert to pascal case
string = toPascalCase(string);
// Convert first character to lowercase
return string.charAt(0).toLowerCase() + string.slice(1);
}
// Remove HTML from a string
export function stripHTML(source) {
const fragment = document.createDocumentFragment();
const element = document.createElement('div');
fragment.appendChild(element);
element.innerHTML = source;
return fragment.firstChild.innerText;
}
// Like outerHTML, but also works for DocumentFragment
export function getHTML(element) {
const wrapper = document.createElement('div');
wrapper.appendChild(element);
return wrapper.innerHTML;
}
+36
View File
@@ -0,0 +1,36 @@
// ==========================================================================
// Time utils
// ==========================================================================
import is from './is';
// Time helpers
export const getHours = value => parseInt((value / 60 / 60) % 60, 10);
export const getMinutes = value => parseInt((value / 60) % 60, 10);
export const getSeconds = value => parseInt(value % 60, 10);
// Format time to UI friendly string
export function formatTime(time = 0, displayHours = false, inverted = false) {
// Bail if the value isn't a number
if (!is.number(time)) {
return formatTime(null, displayHours, inverted);
}
// Format time component to add leading zero
const format = value => `0${value}`.slice(-2);
// Breakdown to hours, mins, secs
let hours = getHours(time);
const mins = getMinutes(time);
const secs = getSeconds(time);
// Do we need to display hours?
if (displayHours || hours > 0) {
hours = `${hours}:`;
} else {
hours = '';
}
// Render
return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
}
+39
View File
@@ -0,0 +1,39 @@
// ==========================================================================
// URL utils
// ==========================================================================
import is from './is';
/**
* Parse a string to a URL object
* @param {string} input - the URL to be parsed
* @param {boolean} safe - failsafe parsing
*/
export function parseUrl(input, safe = true) {
let url = input;
if (safe) {
const parser = document.createElement('a');
parser.href = url;
url = parser.href;
}
try {
return new URL(url);
} catch (e) {
return null;
}
}
// Convert object to URLSearchParams
export function buildUrlParams(input) {
const params = new URLSearchParams();
if (is.object(input)) {
Object.entries(input).forEach(([key, value]) => {
params.set(key, value);
});
}
return params;
}
+1 -1
View File
@@ -21,7 +21,7 @@
transition: transform 0.4s ease-in-out; transition: transform 0.4s ease-in-out;
width: 100%; width: 100%;
span { .plyr__caption {
background: $plyr-captions-bg; background: $plyr-captions-bg;
border-radius: 2px; border-radius: 2px;
box-decoration-break: clone; box-decoration-break: clone;
+4 -4
View File
@@ -34,10 +34,10 @@
} }
// Change icons on state change // Change icons on state change
.plyr__control[aria-pressed='false'] .icon--pressed, .plyr__control:not(.plyr__control--pressed) .icon--pressed,
.plyr__control[aria-pressed='true'] .icon--not-pressed, .plyr__control.plyr__control--pressed .icon--not-pressed,
.plyr__control[aria-pressed='false'] .label--pressed, .plyr__control:not(.plyr__control--pressed) .label--pressed,
.plyr__control[aria-pressed='true'] .label--not-pressed { .plyr__control.plyr__control--pressed .label--not-pressed {
display: none; display: none;
} }
-5
View File
@@ -27,11 +27,6 @@ $embed-padding: ((100 / 16) * 9);
$height: 240; $height: 240;
$offset: to-percentage(($height - $embed-padding) / ($height / 50)); $offset: to-percentage(($height - $embed-padding) / ($height / 50));
// To allow mouse events to be captured if full support
iframe {
pointer-events: none;
}
// Only used for Vimeo // Only used for Vimeo
> .plyr__video-embed__container { > .plyr__video-embed__container {
padding-bottom: to-percentage($height); padding-bottom: to-percentage($height);
+55 -40
View File
@@ -102,6 +102,14 @@
call-me-maybe "^1.0.1" call-me-maybe "^1.0.1"
glob-to-regexp "^0.3.0" glob-to-regexp "^0.3.0"
"@types/estree@0.0.39":
version "0.0.39"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
"@types/node@*":
version "10.3.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.3.3.tgz#8798d9e39af2fa604f715ee6a6b19796528e46c3"
abbrev@1: abbrev@1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
@@ -2616,13 +2624,13 @@ gulp-autoprefixer@^5.0.0:
through2 "^2.0.0" through2 "^2.0.0"
vinyl-sourcemaps-apply "^0.2.0" vinyl-sourcemaps-apply "^0.2.0"
gulp-better-rollup@^3.1.0: gulp-better-rollup@^3.2.1:
version "3.1.0" version "3.2.1"
resolved "https://registry.yarnpkg.com/gulp-better-rollup/-/gulp-better-rollup-3.1.0.tgz#b226ba0c672882075472158b82d22ba9976d4ecb" resolved "https://registry.yarnpkg.com/gulp-better-rollup/-/gulp-better-rollup-3.2.1.tgz#c6fc26c19cd11475c58a4be97e8a7e00f36b3ac2"
dependencies: dependencies:
lodash.camelcase "^4.3.0" lodash.camelcase "^4.3.0"
plugin-error "^0.1.2" plugin-error "^1.0.1"
rollup ">=0.48 <0.57" rollup "^0.60.2"
vinyl "^2.1.0" vinyl "^2.1.0"
vinyl-sourcemaps-apply "^0.2.1" vinyl-sourcemaps-apply "^0.2.1"
@@ -2678,9 +2686,9 @@ gulp-postcss@^7.0.1:
postcss-load-config "^1.2.0" postcss-load-config "^1.2.0"
vinyl-sourcemaps-apply "^0.2.1" vinyl-sourcemaps-apply "^0.2.1"
gulp-rename@^1.2.3: gulp-rename@^1.3.0:
version "1.2.3" version "1.3.0"
resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.2.3.tgz#37b75298e9d3e6c0fe9ac4eac13ce3be5434646b" resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.3.0.tgz#2e789d8f563ab0c924eeb62967576f37ff4cb826"
gulp-replace@^1.0.0: gulp-replace@^1.0.0:
version "1.0.0" version "1.0.0"
@@ -3878,6 +3886,10 @@ lodash@>=3.10.0, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, l
version "4.17.5" version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
lodash@^4.17.10:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
lodash@~1.0.1: lodash@~1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551" resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551"
@@ -4723,9 +4735,9 @@ postcss-html@^0.15.0:
remark "^9.0.0" remark "^9.0.0"
unist-util-find-all-after "^1.0.1" unist-util-find-all-after "^1.0.1"
postcss-html@^0.23.6: postcss-html@^0.28.0:
version "0.23.7" version "0.28.0"
resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.23.7.tgz#47146c15e21b9c00746c40115dcff8270c439f32" resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.28.0.tgz#3dd0f5b5d7f886b8181bf844396d43a7898162cb"
dependencies: dependencies:
htmlparser2 "^3.9.2" htmlparser2 "^3.9.2"
@@ -4735,9 +4747,9 @@ postcss-less@^1.1.0:
dependencies: dependencies:
postcss "^5.2.16" postcss "^5.2.16"
postcss-less@^1.1.5: postcss-less@^2.0.0:
version "1.1.5" version "2.0.0"
resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-1.1.5.tgz#a6f0ce180cf3797eeee1d4adc0e9e6d6db665609" resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-2.0.0.tgz#5d190b8e057ca446d60fe2e2587ad791c9029fb8"
dependencies: dependencies:
postcss "^5.2.16" postcss "^5.2.16"
@@ -4764,9 +4776,9 @@ postcss-load-plugins@^2.3.0:
cosmiconfig "^2.1.1" cosmiconfig "^2.1.1"
object-assign "^4.1.0" object-assign "^4.1.0"
postcss-markdown@^0.23.6: postcss-markdown@^0.28.0:
version "0.23.7" version "0.28.0"
resolved "https://registry.yarnpkg.com/postcss-markdown/-/postcss-markdown-0.23.7.tgz#7e3a398794295c425e51e4f0abdee6d13ad3d134" resolved "https://registry.yarnpkg.com/postcss-markdown/-/postcss-markdown-0.28.0.tgz#99d1c4e74967af9e9c98acb2e2b66df4b3c6ed86"
dependencies: dependencies:
remark "^9.0.0" remark "^9.0.0"
unist-util-find-all-after "^1.0.2" unist-util-find-all-after "^1.0.2"
@@ -4837,9 +4849,9 @@ postcss-sorting@^3.1.0:
lodash "^4.17.4" lodash "^4.17.4"
postcss "^6.0.13" postcss "^6.0.13"
postcss-syntax@^0.9.0: postcss-syntax@^0.28.0:
version "0.9.1" version "0.28.0"
resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.9.1.tgz#5dbd90af1631ab8805b8f594bef2c2e8002d3758" resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.28.0.tgz#e17572a7dcf5388f0c9b68232d2dad48fa7f0b12"
postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0:
version "3.3.0" version "3.3.0"
@@ -4988,9 +5000,9 @@ randomatic@^1.1.3:
is-number "^3.0.0" is-number "^3.0.0"
kind-of "^4.0.0" kind-of "^4.0.0"
raven-js@^3.25.2: raven-js@^3.26.3:
version "3.25.2" version "3.26.3"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.25.2.tgz#d3ad1c694f70855dda6f705204ee6ab76ba62884" resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.26.3.tgz#0efb49969b5b11ab965f7b0d6da4ca102b763cb0"
rc@^1.0.1, rc@^1.1.6: rc@^1.0.1, rc@^1.1.6:
version "1.2.6" version "1.2.6"
@@ -5466,9 +5478,12 @@ rollup-pluginutils@^2.0.1:
estree-walker "^0.3.0" estree-walker "^0.3.0"
micromatch "^2.3.11" micromatch "^2.3.11"
"rollup@>=0.48 <0.57": rollup@^0.60.2:
version "0.56.5" version "0.60.7"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.56.5.tgz#40fe3cf0cd1659d469baad11f4d5b6336c14ce84" resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.60.7.tgz#2b62ef9306f719b1ab85a7814b3e6596ac51fae8"
dependencies:
"@types/estree" "0.0.39"
"@types/node" "*"
run-async@^2.2.0: run-async@^2.2.0:
version "2.3.0" version "2.3.0"
@@ -5888,9 +5903,9 @@ style-search@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902"
stylelint-config-prettier@^3.2.0: stylelint-config-prettier@^3.3.0:
version "3.2.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/stylelint-config-prettier/-/stylelint-config-prettier-3.2.0.tgz#af32b7845adeeddbf0a0bd642ace4ca1e68958e2" resolved "https://registry.yarnpkg.com/stylelint-config-prettier/-/stylelint-config-prettier-3.3.0.tgz#cc22a4b5310c1919cee77131d6e220c60a62a480"
dependencies: dependencies:
stylelint "^9.1.1" stylelint "^9.1.1"
@@ -5923,11 +5938,11 @@ stylelint-scss@^2.0.0:
postcss-selector-parser "^3.1.1" postcss-selector-parser "^3.1.1"
postcss-value-parser "^3.3.0" postcss-value-parser "^3.3.0"
stylelint-scss@^3.1.0: stylelint-scss@^3.1.3:
version "3.1.0" version "3.1.3"
resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.1.0.tgz#aa46503014d1a6edb2fb4c5fefb73a7d0d5bc644" resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.1.3.tgz#28f881ae298c3f5db667b10b6cf94a1a219001d6"
dependencies: dependencies:
lodash "^4.17.4" lodash "^4.17.10"
postcss-media-query-parser "^0.2.3" postcss-media-query-parser "^0.2.3"
postcss-resolve-nested-selector "^0.1.1" postcss-resolve-nested-selector "^0.1.1"
postcss-selector-parser "^4.0.0" postcss-selector-parser "^4.0.0"
@@ -6031,9 +6046,9 @@ stylelint@^8.1.1:
svg-tags "^1.0.0" svg-tags "^1.0.0"
table "^4.0.1" table "^4.0.1"
stylelint@^9.2.1: stylelint@^9.3.0:
version "9.2.1" version "9.3.0"
resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-9.2.1.tgz#fe63c169f6cd3bc81e77f0e3c6443df3267ec211" resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-9.3.0.tgz#fe176e4e421ac10eac1a6b6d9f28e908eb58c5db"
dependencies: dependencies:
autoprefixer "^8.0.0" autoprefixer "^8.0.0"
balanced-match "^1.0.0" balanced-match "^1.0.0"
@@ -6058,9 +6073,9 @@ stylelint@^9.2.1:
normalize-selector "^0.2.0" normalize-selector "^0.2.0"
pify "^3.0.0" pify "^3.0.0"
postcss "^6.0.16" postcss "^6.0.16"
postcss-html "^0.23.6" postcss-html "^0.28.0"
postcss-less "^1.1.5" postcss-less "^2.0.0"
postcss-markdown "^0.23.6" postcss-markdown "^0.28.0"
postcss-media-query-parser "^0.2.3" postcss-media-query-parser "^0.2.3"
postcss-reporter "^5.0.0" postcss-reporter "^5.0.0"
postcss-resolve-nested-selector "^0.1.1" postcss-resolve-nested-selector "^0.1.1"
@@ -6068,7 +6083,7 @@ stylelint@^9.2.1:
postcss-sass "^0.3.0" postcss-sass "^0.3.0"
postcss-scss "^1.0.2" postcss-scss "^1.0.2"
postcss-selector-parser "^3.1.0" postcss-selector-parser "^3.1.0"
postcss-syntax "^0.9.0" postcss-syntax "^0.28.0"
postcss-value-parser "^3.3.0" postcss-value-parser "^3.3.0"
resolve-from "^4.0.0" resolve-from "^4.0.0"
signal-exit "^3.0.2" signal-exit "^3.0.2"