Compare commits

..

195 Commits

Author SHA1 Message Date
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 a4caba120c Merge branch 'master' of github.com:sampotts/plyr
# 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
2018-05-31 23:43:40 +10:00
Sam Potts 969a877a34 v3.3.10 2018-05-31 23:41:48 +10:00
Sam Potts fb22a90d33 Merge pull request #993 from sampotts/develop
v3.3.10
2018-05-31 23:39:51 +10:00
Sam Potts 108bd3dfa0 Fixed incorrect BEM formatting, fixed buffer alignment 2018-05-31 23:33:59 +10:00
Sam Potts 5a445ae647 Merge pull request #988 from kim-company/translate-qualities
Translate quality badges and quality names
2018-05-31 22:43:31 +10:00
Philip Giuliani 56668f58b6 Rename qualityName to label 2018-05-31 14:40:56 +02:00
Sam Potts eec96e5879 Merge pull request #990 from friday/travis
Travis integration
2018-05-31 18:33:00 +10:00
Sam Potts c6c9d877e4 Merge pull request #992 from philipgiuliani/quality-resume-time
Wait for the metadata to be loaded before setting the currentTime
2018-05-31 18:32:10 +10:00
Philip Giuliani 61f4b998e1 Wait for the metadata to be loaded before setting the currentTime 2018-05-31 10:17:41 +02:00
Sam Potts 25a319d884 Merge pull request #989 from friday/pr-template
Proposed changes to PR template
2018-05-31 08:34:02 +10:00
Albin Larsson 4bf678fe6c Proposed changes to PR template (develop as base branch, exclude build and typo fix) 2018-05-30 19:52:15 +02:00
Albin Larsson 359acd6bb9 Lint and build in travis 2018-05-30 19:40:17 +02:00
Albin Larsson 2fce385691 Add npm scripts for linting and building 2018-05-30 19:40:17 +02:00
Albin Larsson a82c61c539 Gulp: Add option to build only 2018-05-30 19:39:57 +02:00
Philip Giuliani a8fa125a96 Refactor getDeep method 2018-05-30 17:30:10 +02:00
Philip Giuliani 6435ced707 Return undefined when the key is not present. 2018-05-30 17:10:38 +02:00
Philip Giuliani 94dc0d176c Rebuild 2018-05-30 17:03:41 +02:00
Philip Giuliani 23c21252e8 Rebuild 2018-05-30 17:02:07 +02:00
Philip Giuliani 41c7dff0e8 Add getDeep method to utils 2018-05-30 17:02:07 +02:00
Philip Giuliani e3bae562fc Rebuild 2018-05-30 17:02:07 +02:00
Philip Giuliani 1c1668bfc3 Implement translation support for qualityName and qualityBadge 2018-05-30 17:02:07 +02:00
Philip Giuliani e0c09c51f2 Allow nested translations 2018-05-30 17:02:07 +02:00
Philip Giuliani 8de06fb862 Accept quality 0 2018-05-30 17:02:07 +02:00
Sam Potts 64412868d8 ESLint tweak 2018-05-30 17:02:07 +02:00
Sam Potts 450958c290 Merge pull request #981 from friday/hls-captions
Improve captions handling for streaming
2018-05-30 21:44:42 +10:00
Sam Potts 963fe11ad6 Update readme.md 2018-05-30 00:22:01 +10:00
Sam Potts ce199e4b6b Merge pull request #986 from friday/701
Call duration update method manually if user config has duration
2018-05-30 00:20:15 +10:00
Albin Larsson 9d798893b5 Call duration update method manually if user config has duration 2018-05-29 16:06:07 +02:00
Albin Larsson 64399e0717 Defer initial captions update to next tick, to avoid event being triggered to early 2018-05-28 17:54:25 +02:00
Albin Larsson f58e23b325 Change to using addtrack and removetrack listeners since 'change' didn't trigger in firefox for embedded captions (may also be a hls.js issue) 2018-05-28 16:19:44 +02:00
Albin Larsson 812e07b734 Replace browser language detection in defaults.js with explicit 'auto' option 2018-05-28 07:43:37 +02:00
Albin Larsson c9298fde76 Fix typo 2018-05-28 07:08:38 +02:00
Albin Larsson 0109454a34 Ensure language is set in case the track is added after initialization, and trigger languagechange event when language is initially set 2018-05-28 06:08:03 +02:00
Albin Larsson 813f703211 Add option to watch caption track changes and update language options 2018-05-28 06:08:03 +02:00
Albin Larsson 7aad747c25 Optimize captions code reused and ensure captionsenabled/captionsdisabled
will be sent on initial setup
2018-05-28 05:44:54 +02:00
Sam Potts d70a787af1 Merge branch 'develop' 2018-05-28 10:42:11 +10:00
Sam Potts 69bb0917ad v3.3.9 2018-05-28 10:41:51 +10:00
Sam Potts 90c5735904 WIP 2018-05-28 10:19:07 +10:00
Sam Potts 6f256d09b2 ESLint tweak 2018-05-28 10:18:04 +10:00
Sam Potts e9684c2021 Merge pull request #979 from friday/respect-storage
Respect storage being disabled for storage getter
2018-05-28 10:09:02 +10:00
Sam Potts bf91a0e73f Merge pull request #977 from friday/utils.is.icue-ie11
Restore utils.is.cue()
2018-05-28 10:08:42 +10:00
Sam Potts 14b6309aef Merge pull request #978 from friday/ie-issues
Fix InvalidStateError and IE11 issues
2018-05-28 10:08:24 +10:00
Albin Larsson 6391ced99f If storage is disabled, disable get as well, not just set 2018-05-28 01:58:06 +02:00
Albin Larsson fac8a185ba Simplify currentTime setter and bail when media hasn't loaded 2018-05-28 00:57:07 +02:00
Albin Larsson c69aa8a42b Avoid duration getter returning NaN before element has loaded 2018-05-28 00:57:01 +02:00
Albin Larsson f34bf22125 Restore utils.is.cue() 2018-05-27 20:28:48 +02:00
Sam Potts f0be913dc3 Merge pull request #975 from sampotts/develop
v3.3.8
2018-05-26 13:55:22 +10:00
Sam Potts cd51788b98 v3.3.8 2018-05-26 13:53:15 +10:00
Sam Potts edd67b0da3 Typo 2018-05-20 23:44:40 +10:00
Sam Potts d733454d7f Pause while seeking 2018-05-20 23:40:28 +10:00
Sam Potts 41f9a87e0e Add URL polyfill 2018-05-20 23:40:00 +10:00
Sam Potts f4858f0c62 Merge pull request #959 from friday/876
Youtube and vimeo fixes
2018-05-19 16:49:31 +10:00
Albin Larsson 121093ae71 Prevent durationchange events from showing time when invertTime is false 2018-05-19 04:27:45 +02:00
Albin Larsson aa8fc313a9 Fix #966: Add 'seeked' event listener to update progress (seeking doesn't have the correct time) 2018-05-19 04:23:22 +02:00
Albin Larsson 723298a07b Fix #921: Trigger seeked event in youtube plugin if either playing or paused 2018-05-19 04:18:27 +02:00
Albin Larsson f8c89e3e95 Fix #876: YouTube and Vimeo autoplays on seek 2018-05-19 04:18:27 +02:00
Albin Larsson 333435a9c2 Fix playback state (paused) and events (play/pause) 2018-05-19 04:18:27 +02:00
Sam Potts 3ab2295fe7 Merge branch 'master' into develop 2018-05-19 11:29:47 +10:00
Sam Potts c41bb657ac Merge pull request #958 from friday/954
Fix the seek tooltip time difference from seek time
2018-05-19 11:28:38 +10:00
Sam Potts 55bbf64f2b Merge pull request #963 from friday/verify-poster
Make sure poster element isn't shown if the image isn't loaded
2018-05-19 11:27:52 +10:00
Sam Potts 3bba65f2c2 Merge pull request #967 from friday/883
toggleControls rewrite
2018-05-19 11:27:19 +10:00
Sam Potts 1bab0d07b5 Merge branch 'master' into develop 2018-05-19 11:26:05 +10:00
Sam Potts 602353f4d9 Merge branch 'master' of github.com:sampotts/plyr 2018-05-19 11:25:02 +10:00
Sam Potts 51814249af Reduce circular dependencies 2018-05-19 11:24:56 +10:00
Albin Larsson 37c5fbfe16 toggleControls() rewrite 2018-05-18 16:19:38 +02:00
Albin Larsson d7356726a1 Remove ui.checkFailed() and error class 2018-05-16 23:21:06 +02:00
Albin Larsson 4db6bf7a2e Make utils.toggleClass() compatible with Element.classList.toggle (rename toggle argument to 'force' and make it optional) 2018-05-16 23:21:06 +02:00
Albin Larsson 28826f6402 Add 'video only' caveat to toggleControls() doc (current behavior) 2018-05-16 23:21:06 +02:00
Albin Larsson c845558d96 Youtube poster: Set css backgroundSize to 'cover' for padded youtube thumbnails 2018-05-15 16:41:51 +02:00
Albin Larsson 16c3a7d9e5 Rewrite ui.setPoster to check that images arent broken or youtube fallback images. Only show poster element when valid 2018-05-15 16:21:36 +02:00
Albin Larsson 90d5b48845 Add async method to utils for loading/checking images 2018-05-15 13:27:55 +02:00
Albin Larsson d1acc4abb3 Add event before seeking via mouse interaction to set alternative 'value' for the input matching the tooltip time 2018-05-14 19:50:08 +02:00
Sam Potts 797b70998f Merge pull request #960 from friday/935
Support importing Plyr in Node.js without errors
2018-05-14 23:38:48 +10:00
Sam Potts 4a01027da0 Merge pull request #961 from friday/expose-defaults
Enable overriding defaults
2018-05-14 23:38:25 +10:00
Albin Larsson 7ca2169790 Expose defaults (enable overriding) 2018-05-14 06:49:04 +02:00
Albin Larsson 054f522aa9 Enable importing Plyr in node.js without errors (resulting in an empty object) 2018-05-14 05:35:13 +02:00
Albin Larsson f2fc3f5ea5 Fix the seek tooltip time difference from seek time 2018-05-12 00:10:39 +02:00
Sam Potts 765c01e83d Remove references to window.Plyr 2018-05-10 09:34:15 +10:00
Sam Potts 33a11fb53a v3.3.7 2018-05-09 09:50:22 +10:00
Sam Potts d1d41ca49a Merge branch 'master' of github.com:sampotts/plyr 2018-05-09 09:48:52 +10:00
Sam Potts c06e0ee5e9 Grid tweak 2018-05-09 09:48:46 +10:00
Sam Potts 83f80ccc40 Merge pull request #950 from friday/poster-fixes
Poster fixes
2018-05-09 09:44:36 +10:00
Albin Larsson 069065ea3a Fix #946 - poster getting click events 2018-05-08 16:50:40 +02:00
Albin Larsson 1672e78041 Fix poster being stretched 2018-05-08 16:49:32 +02:00
Sam Potts 34401de3d0 Merge branch 'master' into develop 2018-05-08 22:22:43 +10:00
Sam Potts f687b81b70 v3.3.6 2018-05-08 13:18:30 +10:00
Sam Potts bbb11e611e Vimeo options, docs for multiple players 2018-05-08 13:12:39 +10:00
Sam Potts 90919411e9 Use div for poster, Vimeo fixes, Tooltip fixes 2018-05-08 12:57:24 +10:00
Sam Potts 1491b017a0 Setup multiple players 2018-05-06 16:18:10 +10:00
Sam Potts 1655150092 v3.3.5 2018-05-06 01:32:51 +10:00
Sam Potts ceb6c9a100 v3.3.3 2018-05-06 01:16:41 +10:00
Sam Potts 00bbce08fb Reverted menu change 2018-05-06 01:14:41 +10:00
Sam Potts 91a4b86860 Small bug fixes 2018-05-06 01:03:38 +10:00
Sam Potts 403df36af6 Merge branch 'master' into develop 2018-04-27 21:42:29 +10:00
Sam Potts 24d833a5d1 Merge branch 'master' into develop 2018-04-27 18:35:06 +10:00
Sam Potts 44b30380f7 Merge branch 'beta' of github.com:Selz/plyr 2018-04-27 18:34:06 +10:00
Sam Potts f13260c10a Merge pull request #919 from sampotts/master
Merge back
2018-04-25 07:38:17 +10:00
Sam Potts e1183d6049 Merge pull request #918 from sampotts/master
Merge back
2018-04-25 07:37:18 +10:00
69 changed files with 29099 additions and 29259 deletions
+1 -2
View File
@@ -32,8 +32,7 @@
"message": "Use local parameter instead."
}
],
"array-bracket-newline": [2, { "minItems": 2 }],
"array-element-newline": [2, { "minItems": 2 }]
"no-param-reassign": [2, { "props": false }]
},
"parserOptions": {
"sourceType": "module"
+5 -5
View File
@@ -1,8 +1,8 @@
### Link to related issue (if applicable)
### Sumary of proposed changes
### Summary of proposed changes
### Task list
- [ ] Tested on [supported browsers](https://github.com/sampotts/plyr#browser-support)
- [ ] Gulp build completed
### Checklist
- [ ] Use `develop` as the base branch
- [ ] Exclude the gulp build from the PR
- [ ] Test on [supported browsers](https://github.com/sampotts/plyr#browser-support)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"useTabs": false,
"tabWidth": 4,
"printWidth": 160,
"printWidth": 120,
"singleQuote": true,
"trailingComma": "all"
}
+7
View File
@@ -0,0 +1,7 @@
language: node_js
node_js:
- 'lts/*'
script:
- npm run lint
- npm run build
+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.
+464 -373
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:
* `Array` of options (this builds the default controls based on your choices)
* `String` containing the desired HTML
* `Function` that will be executed and should return one of the above
- `Array` of options (this builds the default controls based on your choices)
- `String` containing the desired HTML
- `Function` that will be executed and should return one of the above
## 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:
* `{id}` - the dynamically generated ID for the player (for form controls)
* `{seektime}` - the seek time specified in options for fast forward and rewind
* `{title}` - the title of your media, if specified
- `{id}` - the dynamically generated ID for the player (for form controls)
- `{seektime}` - the seek time specified in options for fast forward and rewind
- `{title}` - the title of your media, if specified
### Limitations
* Currently the settings menus are not supported with custom controls HTML
* AirPlay and PiP buttons can be added but you will have to manage feature detection
- Currently the settings menus are not supported with custom controls HTML
- AirPlay and PiP buttons can be added but you will have to manage feature detection
### Example
@@ -105,7 +105,7 @@ const controls = `
<svg role="presentation"><use xlink:href="#plyr-rewind"></use></svg>
<span class="plyr__tooltip" role="tooltip">Rewind {seektime} secs</span>
</button>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Play, {title}" data-plyr="play">
<button type="button" class="plyr__control" aria-label="Play, {title}" data-plyr="play">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-pause"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-play"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Pause</span>
@@ -116,30 +116,28 @@ const controls = `
<span class="plyr__tooltip" role="tooltip">Forward {seektime} secs</span>
</button>
<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" id="plyr-seek-{id}">
<progress class="plyr__progress--buffer" min="0" max="100" value="0">% buffered</progress>
<input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" aria-label="Seek">
<progress class="plyr__progress__buffer" min="0" max="100" value="0">% buffered</progress>
<span role="tooltip" class="plyr__tooltip">00:00</span>
</div>
<div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
<div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute">
<button type="button" class="plyr__control" aria-label="Mute" data-plyr="mute">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Unmute</span>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Mute</span>
</button>
<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" id="plyr-volume-{id}">
<input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" aria-label="Volume">
</div>
<button type="button" class="plyr__control" aria-pressed="true" aria-label="Enable captions" data-plyr="captions">
<button type="button" class="plyr__control" data-plyr="captions">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-captions-off"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Disable captions</span>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enable captions</span>
</button>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Enter fullscreen" data-plyr="fullscreen">
<button type="button" class="plyr__control" data-plyr="fullscreen">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-exit-fullscreen"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-enter-fullscreen"></use></svg>
<span class="label--pressed plyr__tooltip" role="tooltip">Exit fullscreen</span>
+1 -1
View File
File diff suppressed because one or more lines are too long
+4385 -4153
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
+33
View File
@@ -74,6 +74,39 @@ import Raven from 'raven-js';
'airplay',
'fullscreen',
], */
/* i18n: {
restart: '重新開始',
rewind: '快退{seektime}秒',
play: '播放',
pause: '暫停',
fastForward: '快進{seektime}秒',
seek: '尋求',
played: '發揮',
buffered: '緩衝的',
currentTime: '當前時間戳',
duration: '長短',
volume: '音量',
mute: '靜音',
unmute: '取消靜音',
enableCaptions: '開啟字幕',
disableCaptions: '關閉字幕',
enterFullscreen: '進入全螢幕',
exitFullscreen: '退出全螢幕',
frameTitle: '球員為{title}',
captions: '字幕',
settings: '設定',
speed: '速度',
normal: '正常',
quality: '質量',
loop: '循環',
start: 'Start',
end: 'End',
all: 'All',
reset: '重啟',
disabled: '殘',
enabled: '啟用',
advertisement: '廣告',
}, */
captions: {
active: true,
},
+1 -1
View File
@@ -2,4 +2,4 @@
// Layout
// ==========================================================================
$container-max-width: 1280px;
$container-max-width: 1260px;
+1 -1
View File
File diff suppressed because one or more lines are too long
+7695 -7595
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
+13311 -12865
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
+11 -4
View File
@@ -13,6 +13,7 @@ const filter = require('gulp-filter');
const sass = require('gulp-sass');
const cleancss = require('gulp-clean-css');
const run = require('run-sequence');
const header = require('gulp-header');
const prefix = require('gulp-autoprefixer');
const gitbranch = require('git-branch');
const svgstore = require('gulp-svgstore');
@@ -146,6 +147,7 @@ const build = {
options,
),
)
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(output))
.pipe(filter('**/*.js'))
@@ -224,9 +226,14 @@ gulp.task('watch', () => {
gulp.watch(paths.demo.src.sass, tasks.sass);
});
// Build distribution
gulp.task('build', () => {
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite);
});
// Default gulp task
gulp.task('default', () => {
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'watch');
run('build', 'watch');
});
// Publish a version to CDN and demo
@@ -239,11 +246,11 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
const branch = {
current: gitbranch.sync(),
master: 'master',
beta: 'beta',
develop: 'develop',
};
const allowed = [
branch.master,
branch.beta,
branch.develop,
];
const maxAge = 31536000; // 1 year
@@ -255,7 +262,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
},
},
demo: {
uploadPath: branch.current === branch.beta ? 'beta/' : null,
uploadPath: branch.current === branch.develop ? 'beta/' : null,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
Vary: 'Accept-Encoding',
+19 -14
View File
@@ -1,6 +1,6 @@
{
"name": "plyr",
"version": "3.3.0",
"version": "3.3.13",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"main": "./dist/plyr.js",
@@ -8,45 +8,48 @@
"sass": "./src/sass/plyr.scss",
"style": "./dist/plyr.css",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.6.1",
"babel-preset-env": "^1.7.0",
"del": "^3.0.0",
"eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.11.0",
"eslint-plugin-import": "^2.12.0",
"git-branch": "^2.0.1",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^5.0.0",
"gulp-better-rollup": "^3.1.0",
"gulp-clean-css": "^3.9.3",
"gulp-better-rollup": "^3.2.1",
"gulp-clean-css": "^3.9.4",
"gulp-concat": "^2.6.1",
"gulp-filter": "^5.1.0",
"gulp-header": "^2.0.5",
"gulp-open": "^3.0.1",
"gulp-rename": "^1.2.2",
"gulp-replace": "^0.6.1",
"gulp-postcss": "^7.0.1",
"gulp-rename": "^1.3.0",
"gulp-replace": "^1.0.0",
"gulp-s3": "^0.11.0",
"gulp-sass": "^4.0.1",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.4",
"gulp-svgmin": "^1.2.4",
"gulp-svgstore": "^6.1.1",
"gulp-uglify-es": "^1.0.1",
"gulp-uglify-es": "^1.0.4",
"gulp-util": "^3.0.8",
"postcss-custom-properties": "^7.0.0",
"prettier-eslint": "^8.8.1",
"prettier-stylelint": "^0.4.2",
"rollup-plugin-babel": "^3.0.4",
"rollup-plugin-commonjs": "^9.1.0",
"rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-node-resolve": "^3.3.0",
"run-sequence": "^2.2.1",
"stylelint": "^9.2.0",
"stylelint": "^9.3.0",
"stylelint-config-prettier": "^3.2.0",
"stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.0.0",
"stylelint-order": "^0.8.1",
"stylelint-scss": "^3.0.1",
"stylelint-scss": "^3.1.2",
"stylelint-selector-bem-pattern": "^2.0.0"
},
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
@@ -62,6 +65,8 @@
"doc": "readme.md"
},
"scripts": {
"build": "gulp build",
"lint": "eslint src/js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Sam Potts <sam@potts.es>",
@@ -69,7 +74,7 @@
"babel-polyfill": "^6.26.0",
"custom-event-polyfill": "^0.3.0",
"loadjs": "^3.5.4",
"npm": "^6.0.0",
"raven-js": "^3.24.2"
"raven-js": "^3.26.2",
"url-polyfill": "^1.0.13"
}
}
+2 -1
View File
@@ -11,7 +11,8 @@
},
// Exclude from search
"search.exclude": {
"dist/": true
"dist/": true,
"demo/dist/": true
},
// Linting
"stylelint.enable": true,
+36 -25
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`.
### 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
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.
@@ -125,13 +129,17 @@ Include the `plyr.js` script before the closing `</body>` tag and then in your J
See [initialising](#initialising) for more information on advanced setups.
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following:
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
```html
<script src="https://cdn.plyr.io/3.3.0/plyr.js"></script>
<script src="https://cdn.plyr.io/3.3.13/plyr.js"></script>
```
_Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility
...or...
```html
<script src="https://cdn.plyr.io/3.3.13/plyr.polyfilled.js"></script>
```
### CSS
@@ -144,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:
```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.0/plyr.css">
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.13/plyr.css">
```
### SVG Sprite
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.0/plyr.svg`.
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.13/plyr.svg`.
## Ads
@@ -207,10 +215,10 @@ You can specify a range of arguments for the constructor to use:
* A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
* A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
* A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
* A [`NodeList`](https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
* A [jQuery](https://jquery.com) object
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup.
_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.
Here's some examples
@@ -226,20 +234,32 @@ Passing a [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElemen
const player = new Plyr(document.getElementById('player'));
```
Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList):
Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) (see note below):
```javascript
const player = new Plyr(document.querySelectorAll('.js-player'));
```
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds.
##### Setting up multiple players
You have two choices here. You can either use a simple array loop to map the constructor:
```javascript
const players = Array.from(document.querySelectorAll('.js-player')).map(player => new Plyr(player));
const players = Array.from(document.querySelectorAll('.js-player')).map(p => new Plyr(p));
```
...or use a static method where you can pass a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) or an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of elements:
```javascript
const players = Plyr.setup('.js-player');
```
Both options will also return an array of instances in the order of they were in the DOM for the string selector or the source NodeList or Array.
##### Passing options
The second argument for the constructor is the [options](#options) object:
```javascript
@@ -248,7 +268,7 @@ const player = new Plyr('#player', {
});
```
The constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info.
In all cases, the constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info.
#### Options
@@ -287,7 +307,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `invertTime` | Boolean | `true` | Display the current time as a countdown rather than an incremental counter. |
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
| `captions` | Object | `{ active: false, language: window.navigator.language.split('-')[0] }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). |
| `captions` | Object | `{ active: false, language: 'auto', update: false }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). 'auto' uses the browser language. `update`: Listen to changes to tracks and update menu. This is needed for some streaming libraries, but can result in unselectable language options). |
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) |
| `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. |
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
@@ -345,8 +365,9 @@ player.fullscreen.enter(); // Enter fullscreen
| `fullscreen.exit()` | - | Exit fullscreen. |
| `fullscreen.toggle()` | - | Toggle fullscreen. |
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
| `toggleControls(toggle)` | Boolean | Toggle the controls based on the specified boolean. |
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
| `once(event, function)` | String, Function | Add an event listener for the specified event once. |
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
| `supports(type)` | String | Check support for a mime type. |
| `destroy()` | - | Destroy the instance and garbage collect any elements. |
@@ -391,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. |
| `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. |
| `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.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+. |
@@ -591,17 +613,6 @@ document then the shortcuts will work when any element has focus, apart from an
| `C` | Toggle captions |
| `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 in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen).
+282 -168
View File
@@ -6,7 +6,21 @@
import controls from './controls';
import i18n from './i18n';
import support from './support';
import utils from './utils';
import { dedupe } from './utils/arrays';
import browser from './utils/browser';
import {
createElement,
emptyElement,
getAttributesFromSelector,
insertAfter,
removeElement,
toggleClass,
} from './utils/elements';
import { on, triggerEvent } from './utils/events';
import fetch from './utils/fetch';
import is from './utils/is';
import { getHTML } from './utils/strings';
import { parseUrl } from './utils/urls';
const captions = {
// Setup captions
@@ -16,32 +30,14 @@ const captions = {
return;
}
// Set default language if not set
const stored = this.storage.get('language');
if (!utils.is.empty(stored)) {
this.captions.language = stored;
}
if (utils.is.empty(this.captions.language)) {
this.captions.language = this.config.captions.language.toLowerCase();
}
// Set captions enabled state if not set
if (!utils.is.boolean(this.captions.active)) {
const active = this.storage.get('captions');
if (utils.is.boolean(active)) {
this.captions.active = active;
} else {
this.captions.active = this.config.captions.active;
}
}
// Only Vimeo and HTML5 video supported at this point
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
// Clear menu and hide
if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
if (
is.array(this.config.controls) &&
this.config.controls.includes('settings') &&
this.config.settings.includes('captions')
) {
controls.setCaptionsMenu.call(this);
}
@@ -49,26 +45,12 @@ const captions = {
}
// Inject the container
if (!utils.is.element(this.elements.captions)) {
this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions));
if (!is.element(this.elements.captions)) {
this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
utils.insertAfter(this.elements.captions, this.elements.wrapper);
insertAfter(this.elements.captions, this.elements.wrapper);
}
// Set the class hook
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this)));
// Get tracks
const tracks = captions.getTracks.call(this);
// If no caption file exists, hide container for caption text
if (utils.is.empty(tracks)) {
return;
}
// Get browser info
const browser = utils.getBrowser();
// Fix IE captions if CORS is used
// Fetch captions and inject as blobs instead (data URIs not supported!)
if (browser.isIE && window.URL) {
@@ -76,116 +58,274 @@ const captions = {
Array.from(elements).forEach(track => {
const src = track.getAttribute('src');
const href = utils.parseUrl(src);
const url = parseUrl(src);
if (href.hostname !== window.location.href.hostname && [
'http:',
'https:',
].includes(href.protocol)) {
utils
.fetch(src, 'blob')
if (
url !== null &&
url.hostname !== window.location.href.hostname &&
['http:', 'https:'].includes(url.protocol)
) {
fetch(src, 'blob')
.then(blob => {
track.setAttribute('src', window.URL.createObjectURL(blob));
})
.catch(() => {
utils.removeElement(track);
removeElement(track);
});
}
});
}
// Set language
captions.setLanguage.call(this);
// Get and set initial data
// The "preferred" options are not realized unless / until the wanted language has a match
// * languages: Array of user's browser languages.
// * language: The language preferred by user settings or config
// * active: The state preferred by user settings or config
// * toggled: The real captions state
// Enable UI
captions.show.call(this);
const languages = dedupe(
Array.from(navigator.languages || navigator.userLanguage).map(language => language.split('-')[0]),
);
// Set available languages in list
if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
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);
}
Object.assign(this.captions, {
toggled: false,
active,
language,
languages,
});
// Watch changes to textTracks and update captions menu
if (this.isHTML5) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
}
// Update available languages in list next tick (the event must not be triggered before the listeners)
setTimeout(captions.update.bind(this), 0);
},
// Update available language options in settings based on tracks
update() {
const tracks = captions.getTracks.call(this, true);
// Get the wanted language
const { active, language, meta, currentTrackNode } = this.captions;
const languageExists = Boolean(tracks.find(track => track.language === language));
// Handle tracks (add event listener and "pseudo"-default)
if (this.isHTML5 && this.isVideo) {
tracks.filter(track => !meta.get(track)).forEach(track => {
this.debug.log('Track added', track);
// Attempt to store if the original dom element was "default"
meta.set(track, {
default: track.mode === 'showing',
});
// Turn off native caption rendering to avoid double captions
track.mode = 'hidden';
// Add event listener for cue changes
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
});
}
// Update language first time it matches, or if the previous matching track was removed
if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
captions.setLanguage.call(this, language);
captions.toggle.call(this, active && languageExists);
}
// Enable or disable captions based on track length
toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
// Update available languages in list
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
controls.setCaptionsMenu.call(this);
}
},
// Set the captions language
setLanguage() {
// Setup HTML5 track rendering
if (this.isHTML5 && this.isVideo) {
captions.getTracks.call(this).forEach(track => {
// Show track
utils.on(track, 'cuechange', event => captions.setCue.call(this, event));
// Toggle captions display
// Used internally for the toggleCaptions method, with the passive option forced to false
toggle(input, passive = true) {
// If there's no full support
if (!this.supported.ui) {
return;
}
// Turn off native caption rendering to avoid double captions
// eslint-disable-next-line
track.mode = 'hidden';
});
const { toggled } = this.captions; // Current state
const activeClass = this.config.classNames.captions.active;
// Get current track
const currentTrack = captions.getCurrentTrack.call(this);
// Get the next state
// If the method is called without parameter, toggle based on current value
const active = is.nullOrUndefined(input) ? !toggled : input;
// Check if suported kind
if (utils.is.track(currentTrack)) {
// If we change the active track while a cue is already displayed we need to update it
if (Array.from(currentTrack.activeCues || []).length) {
captions.setCue.call(this, currentTrack);
}
// Update state and trigger event
if (active !== toggled) {
// When passive, don't override user preferences
if (!passive) {
this.captions.active = active;
this.storage.set({ captions: active });
}
} 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
getTracks() {
// Return empty array at least
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() {
// Set captions by track index
// Used internally for the currentTrack setter with the passive option forced to false
set(index, passive = true) {
const tracks = captions.getTracks.call(this);
if (!tracks.length) {
return null;
// Disable captions if setting to -1
if (index === -1) {
captions.toggle.call(this, false, passive);
return;
}
// Get track based on current language
let track = tracks.find(track => track.language.toLowerCase() === this.language);
// Get the <track> with default attribute
if (!track) {
track = utils.getElement.call(this, 'track[default]');
if (!is.number(index)) {
this.debug.warn('Invalid caption argument', index);
return;
}
// Get the first track
if (!track) {
[track] = tracks;
if (!(index in tracks)) {
this.debug.warn('Track not found', index);
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
getLabel(track) {
let currentTrack = track;
if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) {
if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
currentTrack = captions.getCurrentTrack.call(this);
}
if (utils.is.track(currentTrack)) {
if (!utils.is.empty(currentTrack.label)) {
if (is.track(currentTrack)) {
if (!is.empty(currentTrack.label)) {
return currentTrack.label;
}
if (!utils.is.empty(currentTrack.language)) {
if (!is.empty(currentTrack.language)) {
return track.language.toUpperCase();
}
@@ -195,74 +335,48 @@ const captions = {
return i18n.get('disabled', this.config);
},
// Display active caption if it contains text
setCue(input) {
// Get the track from the event if needed
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) {
// Update captions using current track's active cues
// Also optional array argument in case there isn't any track (ex: vimeo)
updateCues(input) {
// Requires UI
if (!this.supported.ui) {
return;
}
if (utils.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.textContent = caption.trim();
} else {
content.appendChild(caption);
}
// Set new caption text
this.elements.captions.appendChild(content);
} else {
if (!is.element(this.elements.captions)) {
this.debug.warn('No captions element to render to');
}
},
// Display captions container and button (for initialization)
show() {
// Try to load the value from storage
let active = this.storage.get('captions');
// Otherwise fall back to the default config
if (!utils.is.boolean(active)) {
({ active } = this.config.captions);
} else {
this.captions.active = active;
return;
}
if (active) {
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, true);
utils.toggleState(this.elements.buttons.captions, true);
// 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)
autopause: true,
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
playsinline: true,
// Default time to skip when rewind/fast forward
seekTime: 10,
@@ -56,7 +60,7 @@ const defaults = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.3.0/plyr.svg',
iconUrl: 'https://cdn.plyr.io/3.3.12/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@@ -89,15 +93,7 @@ const defaults = {
// Speed default and options to display
speed: {
selected: 1,
options: [
0.5,
0.75,
1,
1.25,
1.5,
1.75,
2,
],
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
},
// Keyboard shortcut settings
@@ -115,7 +111,10 @@ const defaults = {
// Captions settings
captions: {
active: false,
language: (navigator.language || navigator.userLanguage).split('-')[0],
language: 'auto',
// Listen to new tracks added after Plyr is initialized.
// This is needed for streaming captions, but may result in unselectable options
update: false,
},
// Fullscreen settings
@@ -148,20 +147,17 @@ const defaults = {
'airplay',
'fullscreen',
],
settings: [
'captions',
'quality',
'speed',
],
settings: ['captions', 'quality', 'speed'],
// Localisation
i18n: {
restart: 'Restart',
rewind: 'Rewind {seektime} secs',
rewind: 'Rewind {seektime}s',
play: 'Play',
pause: 'Pause',
fastForward: 'Forward {seektime} secs',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
@@ -176,6 +172,7 @@ const defaults = {
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
@@ -187,6 +184,14 @@ const defaults = {
disabled: 'Disabled',
enabled: 'Enabled',
advertisement: 'Ad',
qualityBadge: {
2160: '4K',
1440: 'HD',
1080: 'HD',
720: 'HD',
576: 'SD',
480: 'SD',
},
},
// URLs
@@ -198,8 +203,8 @@ const defaults = {
},
youtube: {
sdk: 'https://www.youtube.com/iframe_api',
api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
poster: 'https://img.youtube.com/vi/{0}/maxresdefault.jpg,https://img.youtube.com/vi/{0}/hqdefault.jpg',
api:
'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
},
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
@@ -312,13 +317,13 @@ const defaults = {
display: {
currentTime: '.plyr__time--current',
duration: '.plyr__time--duration',
buffer: '.plyr__progress--buffer',
played: '.plyr__progress--played',
loop: '.plyr__progress--loop',
buffer: '.plyr__progress__buffer',
loop: '.plyr__progress__loop', // Used later
volume: '.plyr__volume--display',
},
progress: '.plyr__progress',
captions: '.plyr__captions',
caption: '.plyr__caption',
menu: {
quality: '.js-plyr__menu__list--quality',
},
@@ -326,18 +331,20 @@ const defaults = {
// Class hooks added to the player in different states
classNames: {
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
poster: 'plyr__poster',
ads: 'plyr__ads',
control: 'plyr__control',
type: 'plyr--{0}',
provider: 'plyr--{0}',
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster',
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading',
error: 'plyr--has-error',
hover: 'plyr--hover',
tooltip: 'plyr__tooltip',
cues: 'plyr__cues',
@@ -13,4 +13,22 @@ export const types = {
video: 'video',
};
/**
* Get provider by URL
* @param {string} url
*/
export function getProviderByUrl(url) {
// YouTube
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) {
return providers.youtube;
}
// Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo;
}
return null;
}
export default { providers, types };
+462 -345
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
// ==========================================================================
import utils from './utils';
const browser = utils.getBrowser();
import browser from './utils/browser';
import { hasClass, toggleClass, trapFocus } from './utils/elements';
import { on, triggerEvent } from './utils/events';
import is from './utils/is';
function onChange() {
if (!this.enabled) {
@@ -14,16 +15,16 @@ function onChange() {
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (utils.is.element(button)) {
utils.toggleState(button, this.active);
if (is.element(button)) {
button.pressed = this.active;
}
// Trigger an event
utils.dispatchEvent(this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
// Trap focus in container
if (!browser.isIos) {
utils.trapFocus.call(this.player, this.target, this.active);
trapFocus.call(this.player, this.target, this.active);
}
}
@@ -42,7 +43,7 @@ function toggleFallback(toggle = false) {
document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook
utils.toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Toggle button and fire events
onChange.call(this);
@@ -62,15 +63,20 @@ class Fullscreen {
// Register event listeners
// Handle event (incase user presses escape etc)
utils.on(document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {
// TODO: Filter for target??
onChange.call(this);
});
on.call(
this.player,
document,
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => {
// TODO: Filter for target??
onChange.call(this);
},
);
// Fullscreen toggle on double click
utils.on(this.player.elements.container, 'dblclick', event => {
on.call(this.player, this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls
if (utils.is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return;
}
@@ -83,26 +89,27 @@ class Fullscreen {
// Determine if native supported
static get native() {
return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);
return !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled
);
}
// Get the prefix for handlers
static get prefix() {
// No prefix
if (utils.is.function(document.exitFullscreen)) {
if (is.function(document.exitFullscreen)) {
return '';
}
// Check for fullscreen support by vendor prefix
let value = '';
const prefixes = [
'webkit',
'moz',
'ms',
];
const prefixes = ['webkit', 'moz', 'ms'];
prefixes.some(pre => {
if (utils.is.function(document[`${pre}ExitFullscreen`]) || utils.is.function(document[`${pre}CancelFullScreen`])) {
if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
value = pre;
return true;
}
@@ -135,7 +142,7 @@ class Fullscreen {
// Fallback using classname
if (!Fullscreen.native) {
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
}
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
@@ -145,7 +152,9 @@ class Fullscreen {
// Get target element
get target() {
return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container;
return browser.isIos && this.player.config.fullscreen.iosNative
? this.player.media
: this.player.elements.container;
}
// Update UI
@@ -157,7 +166,7 @@ class Fullscreen {
}
// Add styling hook to show button
utils.toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
}
// Make an element fullscreen
@@ -175,7 +184,7 @@ class Fullscreen {
toggleFallback.call(this, true);
} else if (!this.prefix) {
this.target.requestFullscreen();
} else if (!utils.is.empty(this.prefix)) {
} else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`]();
}
}
@@ -194,7 +203,7 @@ class Fullscreen {
toggleFallback.call(this, false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!utils.is.empty(this.prefix)) {
} else if (!is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.property}`]();
}
+29 -63
View File
@@ -3,40 +3,28 @@
// ==========================================================================
import support from './support';
import utils from './utils';
import { removeElement } from './utils/elements';
import { triggerEvent } from './utils/events';
const html5 = {
getSources() {
if (!this.isHTML5) {
return null;
return [];
}
return this.media.querySelectorAll('source');
const sources = Array.from(this.media.querySelectorAll('source'));
// Filter out unsupported sources
return sources.filter(source => support.mime.call(this, source.getAttribute('type')));
},
// Get quality levels
getQualityOptions() {
if (!this.isHTML5) {
return null;
}
// Get sources
const sources = html5.getSources.call(this);
if (utils.is.empty(sources)) {
return null;
}
// Get <source> with size attribute
const sizes = Array.from(sources).filter(source => !utils.is.empty(source.getAttribute('size')));
// If none, bail
if (utils.is.empty(sizes)) {
return null;
}
// Reduce to unique list
return utils.dedupe(sizes.map(source => Number(source.getAttribute('size'))));
// Get sizes from <source> elements
return html5.getSources
.call(this)
.map(source => Number(source.getAttribute('size')))
.filter(Boolean);
},
extend() {
@@ -51,53 +39,34 @@ const html5 = {
get() {
// Get sources
const sources = html5.getSources.call(player);
const [source] = sources.filter(source => source.getAttribute('src') === player.source);
if (utils.is.empty(sources)) {
return null;
}
const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
if (utils.is.empty(matches)) {
return null;
}
return Number(matches[0].getAttribute('size'));
// Return size, if match is found
return source && Number(source.getAttribute('size'));
},
set(input) {
// Get sources
const sources = html5.getSources.call(player);
if (utils.is.empty(sources)) {
// Get first match for requested size
const source = sources.find(source => Number(source.getAttribute('size')) === input);
// No matching source found
if (!source) {
return;
}
// Get matches for requested size
const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input);
// No matches for requested size
if (utils.is.empty(matches)) {
return;
}
// Get supported sources
const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type')));
// No supported sources
if (utils.is.empty(supported)) {
return;
}
// Trigger change event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
quality: input,
});
// Get current state
const { currentTime, playing } = player;
// Set new source
player.media.src = supported[0].getAttribute('src');
player.media.src = source.getAttribute('src');
// Restore time
const onLoadedMetaData = () => {
player.currentTime = currentTime;
};
player.once('loadedmetadata', onLoadedMetaData);
// Load new source
player.media.load();
@@ -107,11 +76,8 @@ const html5 = {
player.play();
}
// Restore time
player.currentTime = currentTime;
// Trigger change event
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: input,
});
},
@@ -126,7 +92,7 @@ const html5 = {
}
// Remove child sources
utils.removeElement(html5.getSources());
removeElement(html5.getSources.call(this));
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
+11 -8
View File
@@ -2,26 +2,29 @@
// Plyr internationalization
// ==========================================================================
import utils from './utils';
import is from './utils/is';
import { getDeep } from './utils/objects';
import { replaceAll } from './utils/strings';
const i18n = {
get(key = '', config = {}) {
if (utils.is.empty(key) || utils.is.empty(config) || !Object.keys(config.i18n).includes(key)) {
if (is.empty(key) || is.empty(config)) {
return '';
}
let string = config.i18n[key];
let string = getDeep(config.i18n, key);
if (is.empty(string)) {
return '';
}
const replace = {
'{seektime}': config.seekTime,
'{title}': config.title,
};
Object.entries(replace).forEach(([
key,
value,
]) => {
string = utils.replaceAll(string, key, value);
Object.entries(replace).forEach(([key, value]) => {
string = replaceAll(string, key, value);
});
return string;
+211 -150
View File
@@ -4,10 +4,10 @@
import controls from './controls';
import ui from './ui';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
import browser from './utils/browser';
import { getElement, getElements, getFocusElement, matches, toggleClass, toggleHidden } from './utils/elements';
import { on, once, toggleListener, triggerEvent } from './utils/events';
import is from './utils/is';
class Listeners {
constructor(player) {
@@ -32,7 +32,7 @@ class Listeners {
// If the event is bubbled from the media element
// Firefox doesn't get the keycode for whatever reason
if (!utils.is.number(code)) {
if (!is.number(code)) {
return;
}
@@ -46,35 +46,17 @@ class Listeners {
// Reset on keyup
if (pressed) {
// Which keycodes should we prevent default
const preventDefault = [
48,
49,
50,
51,
52,
53,
54,
56,
57,
32,
75,
38,
40,
77,
39,
37,
70,
67,
73,
76,
79,
];
const preventDefault = [32, 37, 38, 39, 40, 48, 49, 50, 51, 52, 53, 54, 56, 57, 67, 70, 73, 75, 76, 77, 79];
// Check focused element
// and if the focused element is not editable (e.g. text input)
// and any that accept key input http://webaim.org/techniques/keyboard/
const focused = utils.getFocusElement();
if (utils.is.element(focused) && utils.matches(focused, this.player.config.selectors.editable)) {
const focused = getFocusElement();
if (
is.element(focused) &&
(focused !== this.player.elements.inputs.seek &&
matches(focused, this.player.config.selectors.editable))
) {
return;
}
@@ -192,41 +174,37 @@ class Listeners {
this.player.touch = true;
// Add touch class
utils.toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
// Clean up
utils.off(document.body, 'touchstart', this.firstTouch);
toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
}
// Global window & document listeners
global(toggle = true) {
// Keyboard shortcuts
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
utils.toggleListener(document.body, 'click', this.toggleMenu, toggle);
toggleListener.call(this.player, document.body, 'click', this.toggleMenu, toggle);
// Detect touch by events
utils.on(document.body, 'touchstart', this.firstTouch);
once.call(this.player, document.body, 'touchstart', this.firstTouch);
}
// Container listeners
container() {
// Keyboard shortcuts
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
// Remove class on blur/focusout
utils.on(this.player.elements.container, 'focusout', event => {
utils.toggleClass(event.target, this.player.config.classNames.tabFocus, false);
on.call(this.player, this.player.elements.container, 'focusout', event => {
toggleClass(event.target, this.player.config.classNames.tabFocus, false);
});
// 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) {
return;
}
@@ -234,36 +212,64 @@ class Listeners {
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
utils.toggleClass(utils.getFocusElement(), this.player.config.classNames.tabFocus, true);
toggleClass(getFocusElement(), this.player.config.classNames.tabFocus, true);
}, 0);
});
// Toggle controls visibility based on mouse movement
if (this.player.config.hideControls) {
// Toggle controls on mouse events and entering fullscreen
utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', event => {
this.player.toggleControls(event);
});
}
// Toggle controls on mouse events and entering fullscreen
on.call(
this.player,
this.player.elements.container,
'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',
event => {
const { controls } = this.player.elements;
// Remove button states for fullscreen
if (event.type === 'enterfullscreen') {
controls.pressed = false;
controls.hover = false;
}
// Show, then hide after a timeout unless another control event occurs
const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
let delay = 0;
if (show) {
ui.toggleControls.call(this.player, true);
// Use longer timeout for touch devices
delay = this.player.touch ? 3000 : 2000;
}
// Clear timer
clearTimeout(this.player.timers.controls);
// Timer to prevent flicker when seeking
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
},
);
}
// Listen for media events
media() {
// Time change on media
utils.on(this.player.media, 'timeupdate seeking', event => ui.timeUpdate.call(this.player, event));
on.call(this.player, this.player.media, 'timeupdate seeking seeked', event =>
controls.timeUpdate.call(this.player, event),
);
// Display duration
utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => ui.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
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
utils.on(this.player.media, 'loadeddata', () => {
utils.toggleHidden(this.player.elements.volume, !this.player.hasAudio);
utils.toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio);
on.call(this.player, this.player.media, 'canplay', () => {
toggleHidden(this.player.elements.volume, !this.player.hasAudio);
toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio);
});
// Handle the media finishing
utils.on(this.player.media, 'ended', () => {
on.call(this.player, this.player.media, 'ended', () => {
// Show poster on end
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
// Restart
@@ -272,23 +278,28 @@ class Listeners {
});
// Check for buffer progress
utils.on(this.player.media, 'progress playing', event => ui.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
utils.on(this.player.media, 'volumechange', event => ui.updateVolume.call(this.player, event));
on.call(this.player, this.player.media, 'volumechange', event =>
controls.updateVolume.call(this.player, event),
);
// 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
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
// Check if media failed to load
// utils.on(this.player.media, 'play', event => ui.checkFailed.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
// 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) {
return;
}
@@ -303,15 +314,15 @@ class Listeners {
// Click video
if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) {
// 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)
if (!utils.is.element(wrapper)) {
if (!is.element(wrapper)) {
return;
}
// 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)
if (this.player.config.hideControls && this.player.touch && !this.player.paused) {
return;
@@ -330,7 +341,8 @@ class Listeners {
// Disable right click
if (this.player.supported.ui && this.player.config.disableContextMenu) {
utils.on(
on.call(
this.player,
this.player.elements.wrapper,
'contextmenu',
event => {
@@ -341,13 +353,13 @@ class Listeners {
}
// Volume change
utils.on(this.player.media, 'volumechange', () => {
on.call(this.player, this.player.media, 'volumechange', () => {
// Save to storage
this.player.storage.set({ volume: this.player.volume, muted: this.player.muted });
});
// Speed change
utils.on(this.player.media, 'ratechange', () => {
on.call(this.player, this.player.media, 'ratechange', () => {
// Update UI
controls.updateSetting.call(this.player, 'speed');
@@ -356,49 +368,29 @@ class Listeners {
});
// Quality request
utils.on(this.player.media, 'qualityrequested', event => {
on.call(this.player, this.player.media, 'qualityrequested', event => {
// Save to storage
this.player.storage.set({ quality: event.detail.quality });
});
// Quality change
utils.on(this.player.media, 'qualitychange', event => {
on.call(this.player, this.player.media, 'qualitychange', event => {
// Update UI
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
// Bubble up key events for Edge
utils.on(this.player.media, this.player.config.events.concat([
'keyup',
'keydown',
]).join(' '), event => {
let detail = {};
const proxyEvents = this.player.config.events.concat(['keyup', 'keydown']).join(' ');
on.call(this.player, this.player.media, proxyEvents, event => {
let { detail = {} } = event;
// Get error details from media
if (event.type === '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);
});
}
@@ -410,7 +402,7 @@ class Listeners {
// Run default and custom handlers
const proxy = (event, defaultHandler, customHandlerKey) => {
const customHandler = this.player.config.listeners[customHandlerKey];
const hasCustomHandler = utils.is.function(customHandler);
const hasCustomHandler = is.function(customHandler);
let returned = true;
// Execute custom handler
@@ -419,33 +411,41 @@ class Listeners {
}
// 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);
}
};
// 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 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
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
on(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
// Rewind
on(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind');
bind(this.player.elements.buttons.rewind, 'click', this.player.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
on(
bind(
this.player.elements.buttons.mute,
'click',
() => {
@@ -455,10 +455,10 @@ class Listeners {
);
// Captions toggle
on(this.player.elements.buttons.captions, 'click', this.player.toggleCaptions);
bind(this.player.elements.buttons.captions, 'click', () => this.player.toggleCaptions());
// Fullscreen toggle
on(
bind(
this.player.elements.buttons.fullscreen,
'click',
() => {
@@ -468,7 +468,7 @@ class Listeners {
);
// Picture-in-Picture
on(
bind(
this.player.elements.buttons.pip,
'click',
() => {
@@ -478,15 +478,15 @@ class Listeners {
);
// 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
on(this.player.elements.buttons.settings, 'click', event => {
bind(this.player.elements.buttons.settings, 'click', event => {
controls.toggleMenu.call(this.player, event);
});
// Settings menu
on(this.player.elements.settings.form, 'click', event => {
bind(this.player.elements.settings.form, 'click', event => {
event.stopPropagation();
// Go back to home tab on click
@@ -496,16 +496,16 @@ class Listeners {
};
// 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(
event,
() => {
this.player.language = event.target.value;
this.player.currentTrack = Number(event.target.value);
showHomeTab();
},
'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(
event,
() => {
@@ -514,7 +514,7 @@ class Listeners {
},
'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(
event,
() => {
@@ -529,32 +529,77 @@ class Listeners {
}
});
// Set range input alternative "value", which matches the tooltip time (#954)
bind(this.player.elements.inputs.seek, 'mousedown mousemove', event => {
const clientRect = this.player.elements.progress.getBoundingClientRect();
const percent = 100 / clientRect.width * (event.pageX - clientRect.left);
event.currentTarget.setAttribute('seek-value', percent);
});
// Pause while seeking
bind(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
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?
const play = seek.hasAttribute('play-on-seeked');
// Done seeking
const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
// If we're done seeking and it was playing, resume playback
if (play && done) {
seek.removeAttribute('play-on-seeked');
this.player.play();
} else if (!done && this.player.playing) {
seek.setAttribute('play-on-seeked', '');
this.player.pause();
}
});
// Seek
on(
bind(
this.player.elements.inputs.seek,
inputEvent,
event => {
this.player.currentTime = event.target.value / event.target.max * this.player.duration;
const seek = event.currentTarget;
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
let seekTo = seek.getAttribute('seek-value');
if (is.empty(seekTo)) {
seekTo = seek.value;
}
seek.removeAttribute('seek-value');
this.player.currentTime = seekTo / seek.max * this.player.duration;
},
'seek',
);
// Current time invert
// 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)) {
on(this.player.elements.display.currentTime, 'click', () => {
if (this.player.config.toggleInvert && !is.element(this.player.elements.display.duration)) {
bind(this.player.elements.display.currentTime, 'click', () => {
// Do nothing if we're at the start
if (this.player.currentTime === 0) {
return;
}
this.player.config.invertTime = !this.player.config.invertTime;
ui.timeUpdate.call(this.player);
controls.timeUpdate.call(this.player);
});
}
// Volume
on(
bind(
this.player.elements.inputs.volume,
inputEvent,
event => {
@@ -565,37 +610,55 @@ class Listeners {
// Polyfill for lower fill in <input type="range"> for webkit
if (browser.isWebkit) {
on(utils.getElements.call(this.player, 'input[type="range"]'), 'input', event => {
controls.updateRangeFill.call(this.player, event.target);
Array.from(getElements.call(this.player, 'input[type="range"]')).forEach(element => {
bind(element, 'input', event => controls.updateRangeFill.call(this.player, event.target));
});
}
// 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),
);
// Toggle controls visibility based on mouse movement
if (this.player.config.hideControls) {
// Watch for cursor over controls so they don't hide when trying to interact
on(this.player.elements.controls, 'mouseenter mouseleave', event => {
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
});
// Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
bind(this.player.elements.controls, 'mouseenter mouseleave', event => {
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
});
// Watch for cursor over controls so they don't hide when trying to interact
on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
this.player.elements.controls.pressed = [
'mousedown',
'touchstart',
].includes(event.type);
});
// Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
bind(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
this.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
});
// Focus in/out on controls
on(this.player.elements.controls, 'focusin focusout', event => {
this.player.toggleControls(event);
});
}
// Focus in/out on controls
bind(this.player.elements.controls, 'focusin focusout', event => {
const { config, elements, timers } = this.player;
// Skip transition to prevent focus from scrolling the parent element
toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin');
// Toggle
ui.toggleControls.call(this.player, event.type === 'focusin');
// If focusin, hide again after delay
if (event.type === 'focusin') {
// Restore transition
setTimeout(() => {
toggleClass(elements.controls, config.classNames.noTransition, false);
}, 0);
// Delay a little more for keyboard users
const delay = this.touch ? 3000 : 4000;
// Clear timer
clearTimeout(timers.controls);
// Hide
timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
}
});
// Mouse wheel for volume
on(
bind(
this.player.elements.inputs.volume,
'wheel',
event => {
@@ -628,7 +691,10 @@ class Listeners {
}
// Don't break page scrolling at max and min
if ((direction === 1 && this.player.media.volume < 1) || (direction === -1 && this.player.media.volume > 0)) {
if (
(direction === 1 && this.player.media.volume < 1) ||
(direction === -1 && this.player.media.volume > 0)
) {
event.preventDefault();
}
},
@@ -636,11 +702,6 @@ class Listeners {
false,
);
}
// Reset on destroy
clear() {
this.global(false);
}
}
export default Listeners;
+12 -21
View File
@@ -5,7 +5,7 @@
import html5 from './html5';
import vimeo from './plugins/vimeo';
import youtube from './plugins/youtube';
import utils from './utils';
import { createElement, toggleClass, wrap } from './utils/elements';
const media = {
// Setup media
@@ -17,50 +17,41 @@ const media = {
}
// Add type class
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
// Add provider class
utils.toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
// Add video class for embeds
// This will require changes if audio embeds are added
if (this.isEmbed) {
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
}
// Inject the player wrapper
if (this.isVideo) {
// Create the wrapper div
this.elements.wrapper = utils.createElement('div', {
this.elements.wrapper = createElement('div', {
class: this.config.classNames.video,
});
// Wrap the video in a container
utils.wrap(this.media, this.elements.wrapper);
wrap(this.media, this.elements.wrapper);
// Faux poster container
this.elements.poster = utils.createElement('span', {
this.elements.poster = createElement('div', {
class: this.config.classNames.poster,
});
this.elements.wrapper.appendChild(this.elements.poster);
}
if (this.isEmbed) {
switch (this.provider) {
case 'youtube':
youtube.setup.call(this);
break;
case 'vimeo':
vimeo.setup.call(this);
break;
default:
break;
}
} else if (this.isHTML5) {
if (this.isHTML5) {
html5.extend.call(this);
} else if (this.isYouTube) {
youtube.setup.call(this);
} else if (this.isVimeo) {
vimeo.setup.call(this);
}
},
};
+29 -18
View File
@@ -7,7 +7,12 @@
/* global google */
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 {
/**
@@ -18,7 +23,6 @@ class Ads {
constructor(player) {
this.player = player;
this.publisherId = player.config.ads.publisherId;
this.enabled = player.isHTML5 && player.isVideo && player.config.ads.enabled && utils.is.string(this.publisherId) && this.publisherId.length;
this.playing = false;
this.initialized = false;
this.elements = {
@@ -44,15 +48,18 @@ class Ads {
this.load();
}
get enabled() {
return this.player.isVideo && this.player.config.ads.enabled && !is.empty(this.publisherId);
}
/**
* Load the IMA SDK
*/
load() {
if (this.enabled) {
// Check if the Google IMA3 SDK is loaded or load it ourselves
if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
utils
.loadScript(this.player.config.urls.googleIMA.sdk)
if (!is.object(window.google) || !is.object(window.google.ima)) {
loadScript(this.player.config.urls.googleIMA.sdk)
.then(() => {
this.ready();
})
@@ -100,7 +107,7 @@ class Ads {
const base = 'https://go.aniview.com/api/adserver6/vast/';
return `${base}?${utils.buildUrlParams(params)}`;
return `${base}?${buildUrlParams(params)}`;
}
/**
@@ -113,7 +120,7 @@ class Ads {
*/
setupIMA() {
// Create the container for our advertisements
this.elements.container = utils.createElement('div', {
this.elements.container = createElement('div', {
class: this.player.config.classNames.ads,
});
this.player.elements.container.appendChild(this.elements.container);
@@ -143,7 +150,11 @@ class Ads {
this.loader = new google.ima.AdsLoader(this.elements.displayContainer);
// Listen and respond to ads loaded and error events
this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false);
this.loader.addEventListener(
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
event => this.onAdsManagerLoaded(event),
false,
);
this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);
// Request video ads
@@ -181,7 +192,7 @@ class Ads {
}
const update = () => {
const time = utils.formatTime(Math.max(this.manager.getRemainingTime(), 0));
const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));
const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;
this.elements.container.setAttribute('data-badge-text', label);
};
@@ -209,14 +220,14 @@ class Ads {
this.cuePoints = this.manager.getCuePoints();
// Add advertisement cue's within the time line if available
if (!utils.is.empty(this.cuePoints)) {
if (!is.empty(this.cuePoints)) {
this.cuePoints.forEach(cuePoint => {
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
const seekElement = this.player.elements.progress;
if (utils.is.element(seekElement)) {
if (is.element(seekElement)) {
const cuePercentage = 100 / this.player.duration * cuePoint;
const cue = utils.createElement('span', {
const cue = createElement('span', {
class: this.player.config.classNames.cues,
});
@@ -263,7 +274,7 @@ class Ads {
// Proxy event
const dispatchEvent = type => {
const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
utils.dispatchEvent.call(this.player, this.player.media, event);
triggerEvent.call(this.player, this.player.media, event);
};
switch (event.type) {
@@ -390,7 +401,7 @@ class Ads {
this.player.on('seeked', () => {
const seekedTime = this.player.currentTime;
if (utils.is.empty(this.cuePoints)) {
if (is.empty(this.cuePoints)) {
return;
}
@@ -527,9 +538,9 @@ class Ads {
trigger(event, ...args) {
const handlers = this.events[event];
if (utils.is.array(handlers)) {
if (is.array(handlers)) {
handlers.forEach(handler => {
if (utils.is.function(handler)) {
if (is.function(handler)) {
handler.apply(this, args);
}
});
@@ -543,7 +554,7 @@ class Ads {
* @return {Ads}
*/
on(event, callback) {
if (!utils.is.array(this.events[event])) {
if (!is.array(this.events[event])) {
this.events[event] = [];
}
@@ -574,7 +585,7 @@ class Ads {
* @param {string} from
*/
clearSafetyTimer(from) {
if (!utils.is.nullOrUndefined(this.safetyTimer)) {
if (!is.nullOrUndefined(this.safetyTimer)) {
this.player.debug.log(`Safety timer cleared from: ${from}`);
clearTimeout(this.safetyTimer);
+116 -81
View File
@@ -5,20 +5,57 @@
import captions from './../captions';
import controls from './../controls';
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)
function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) {
this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
const vimeo = {
setup() {
// Add embed class for responsive
utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set intial ratio
vimeo.setAspectRatio.call(this);
// Load the API if not already
if (!utils.is.object(window.Vimeo)) {
utils
.loadScript(this.config.urls.vimeo.sdk)
if (!is.object(window.Vimeo)) {
loadScript(this.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(this);
})
@@ -33,8 +70,8 @@ const vimeo = {
// Set aspect ratio
// For Vimeo we have an extra 300% height <div> to hide the standard controls and UI
setAspectRatio(input) {
const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':');
const padding = 100 / ratio[0] * ratio[1];
const [x, y] = (is.string(input) ? input : this.config.ratio).split(':');
const padding = 100 / x * y;
this.elements.wrapper.style.paddingBottom = `${padding}%`;
if (this.supported.ui) {
@@ -53,6 +90,7 @@ const vimeo = {
const options = {
loop: player.config.loop.active,
autoplay: player.autoplay,
// muted: player.muted,
byline: false,
portrait: false,
title: false,
@@ -61,34 +99,37 @@ const vimeo = {
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
};
const params = utils.buildUrlParams(options);
const params = buildUrlParams(options);
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (utils.is.empty(source)) {
if (is.empty(source)) {
source = player.media.getAttribute(player.config.attributes.embed.id);
}
const id = utils.parseVimeoId(source);
const id = parseId(source);
// Build an iframe
const iframe = utils.createElement('iframe');
const src = utils.format(player.config.urls.vimeo.iframe, id, params);
const iframe = createElement('iframe');
const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay');
// Get poster, if already set
const { poster } = player;
// Inject the package
const wrapper = utils.createElement('div');
const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe);
player.media = utils.replaceElement(wrapper, player.media);
player.media = replaceElement(wrapper, player.media);
// Get poster image
utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (utils.is.empty(response)) {
fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (is.empty(response)) {
return;
}
@@ -98,16 +139,16 @@ const vimeo = {
// Get original image
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// Set attribute
player.media.setAttribute('poster', url.href);
// Update
ui.setPoster.call(player);
// Set and show poster
ui.setPoster.call(player, url.href).catch(() => {});
});
// Setup instance
// https://github.com/vimeo/player.js
player.embed = new window.Vimeo.Player(iframe);
player.embed = new window.Vimeo.Player(iframe, {
autopause: player.config.autopause,
muted: player.muted,
});
player.media.paused = true;
player.media.currentTime = 0;
@@ -119,15 +160,13 @@ const vimeo = {
// Create a faux HTML5 API using the Vimeo API
player.media.play = () => {
player.embed.play().then(() => {
player.media.paused = false;
});
assurePlaybackState.call(player, true);
return player.embed.play();
};
player.media.pause = () => {
player.embed.pause().then(() => {
player.media.paused = true;
});
assurePlaybackState.call(player, false);
return player.embed.pause();
};
player.media.stop = () => {
@@ -142,25 +181,27 @@ const vimeo = {
return currentTime;
},
set(time) {
// Get current paused state
// Vimeo will automatically play on seek
const { paused } = player.media;
// Vimeo will automatically play on seek if the video hasn't been played before
// Set seeking flag
player.media.seeking = true;
// Get current paused state and volume etc
const { embed, media, paused, volume } = player;
const restorePause = paused && !embed.hasPlayed;
// Trigger seeking
utils.dispatchEvent.call(player, player.media, 'seeking');
// Set seeking state and trigger event
media.seeking = true;
triggerEvent.call(player, media, 'seeking');
// Seek after events
player.embed.setCurrentTime(time).catch(() => {
// Do nothing
});
// Restore pause state
if (paused) {
player.pause();
}
// If paused, mute until seek is complete
Promise.resolve(restorePause && embed.setVolume(0))
// Seek
.then(() => embed.setCurrentTime(time))
// Restore paused
.then(() => restorePause && embed.pause())
// Restore volume
.then(() => restorePause && embed.setVolume(volume))
.catch(() => {
// Do nothing
});
},
});
@@ -175,7 +216,7 @@ const vimeo = {
.setPlaybackRate(input)
.then(() => {
speed = input;
utils.dispatchEvent.call(player, player.media, 'ratechange');
triggerEvent.call(player, player.media, 'ratechange');
})
.catch(error => {
// Hide menu item (and menu if empty)
@@ -195,7 +236,7 @@ const vimeo = {
set(input) {
player.embed.setVolume(input).then(() => {
volume = input;
utils.dispatchEvent.call(player, player.media, 'volumechange');
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
@@ -207,11 +248,11 @@ const vimeo = {
return muted;
},
set(input) {
const toggle = utils.is.boolean(input) ? input : false;
const toggle = is.boolean(input) ? input : false;
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
muted = toggle;
utils.dispatchEvent.call(player, player.media, 'volumechange');
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
@@ -223,7 +264,7 @@ const vimeo = {
return loop;
},
set(input) {
const toggle = utils.is.boolean(input) ? input : player.config.loop.active;
const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => {
loop = toggle;
@@ -256,11 +297,8 @@ const vimeo = {
});
// Set aspect ratio based on video size
Promise.all([
player.embed.getVideoWidth(),
player.embed.getVideoHeight(),
]).then(dimensions => {
const ratio = utils.getAspectRatio(dimensions[0], dimensions[1]);
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
const ratio = getAspectRatio(dimensions[0], dimensions[1]);
vimeo.setAspectRatio.call(this, ratio);
});
@@ -278,13 +316,13 @@ const vimeo = {
// Get current time
player.embed.getCurrentTime().then(value => {
currentTime = value;
utils.dispatchEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'timeupdate');
});
// Get duration
player.embed.getDuration().then(value => {
player.media.duration = value;
utils.dispatchEvent.call(player, player.media, 'durationchange');
triggerEvent.call(player, player.media, 'durationchange');
});
// Get captions
@@ -293,18 +331,21 @@ const vimeo = {
captions.setup.call(player);
});
player.embed.on('cuechange', data => {
let cue = null;
if (data.cues.length) {
cue = utils.stripHTML(data.cues[0].text);
}
captions.setText.call(player, cue);
player.embed.on('cuechange', ({ cues = [] }) => {
const strippedCues = cues.map(cue => stripHTML(cue.text));
captions.updateCues.call(player, strippedCues);
});
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;
// Fix keyboard focus issues
@@ -314,32 +355,27 @@ const vimeo = {
});
player.embed.on('play', () => {
// Only fire play if paused before
if (player.media.paused) {
utils.dispatchEvent.call(player, player.media, 'play');
}
player.media.paused = false;
utils.dispatchEvent.call(player, player.media, 'playing');
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('pause', () => {
player.media.paused = true;
utils.dispatchEvent.call(player, player.media, 'pause');
assurePlaybackState.call(player, false);
});
player.embed.on('timeupdate', data => {
player.media.seeking = false;
currentTime = data.seconds;
utils.dispatchEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'timeupdate');
});
player.embed.on('progress', data => {
player.media.buffered = data.percent;
utils.dispatchEvent.call(player, player.media, 'progress');
triggerEvent.call(player, player.media, 'progress');
// Check all loaded
if (parseInt(data.percent, 10) === 1) {
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
triggerEvent.call(player, player.media, 'canplaythrough');
}
// Get duration as if we do it before load, it gives an incorrect value
@@ -347,25 +383,24 @@ const vimeo = {
player.embed.getDuration().then(value => {
if (value !== player.media.duration) {
player.media.duration = value;
utils.dispatchEvent.call(player, player.media, 'durationchange');
triggerEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('seeked', () => {
player.media.seeking = false;
utils.dispatchEvent.call(player, player.media, 'seeked');
utils.dispatchEvent.call(player, player.media, 'play');
triggerEvent.call(player, player.media, 'seeked');
});
player.embed.on('ended', () => {
player.media.paused = true;
utils.dispatchEvent.call(player, player.media, 'ended');
triggerEvent.call(player, player.media, 'ended');
});
player.embed.on('error', detail => {
player.media.error = detail;
utils.dispatchEvent.call(player, player.media, 'error');
triggerEvent.call(player, player.media, 'error');
});
// Rebuild UI
+160 -168
View File
@@ -4,80 +4,81 @@
import controls from './../controls';
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
function mapQualityUnit(input) {
switch (input) {
case 'hd2160':
return 2160;
const qualities = {
hd2160: 2160,
hd1440: 1440,
hd1080: 1080,
hd720: 720,
large: 480,
medium: 360,
small: 240,
tiny: 144,
};
case 2160:
return 'hd2160';
const entry = Object.entries(qualities).find(entry => entry.includes(input));
case 'hd1440':
return 1440;
case 1440:
return 'hd1440';
case 'hd1080':
return 1080;
case 1080:
return 'hd1080';
case 'hd720':
return 720;
case 720:
return 'hd720';
case 'large':
return 480;
case 480:
return 'large';
case 'medium':
return 360;
case 360:
return 'medium';
case 'small':
return 240;
case 240:
return 'small';
default:
return 'default';
if (entry) {
// Get the match corresponding to the input
return entry.find(value => value !== input);
}
return 'default';
}
function mapQualityUnits(levels) {
if (utils.is.empty(levels)) {
if (is.empty(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)
function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) {
this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
const youtube = {
setup() {
// Add embed class for responsive
utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set aspect ratio
youtube.setAspectRatio.call(this);
// Setup API
if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) {
if (is.object(window.YT) && is.function(window.YT.Player)) {
youtube.ready.call(this);
} else {
// Load the API
utils.loadScript(this.config.urls.youtube.sdk).catch(error => {
loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
@@ -104,10 +105,10 @@ const youtube = {
// Try via undocumented API method first
// This method disappears now and then though...
// https://github.com/sampotts/plyr/issues/709
if (utils.is.function(this.embed.getVideoData)) {
if (is.function(this.embed.getVideoData)) {
const { title } = this.embed.getVideoData();
if (utils.is.empty(title)) {
if (is.empty(title)) {
this.config.title = title;
ui.setTitle.call(this);
return;
@@ -116,13 +117,12 @@ const youtube = {
// Or via Google API
const key = this.config.keys.google;
if (utils.is.string(key) && !utils.is.empty(key)) {
const url = utils.format(this.config.urls.youtube.api, videoId, key);
if (is.string(key) && !is.empty(key)) {
const url = format(this.config.urls.youtube.api, videoId, key);
utils
.fetch(url)
fetch(url)
.then(result => {
if (utils.is.object(result)) {
if (is.object(result)) {
this.config.title = result.items[0].snippet.title;
ui.setTitle.call(this);
}
@@ -143,7 +143,7 @@ const youtube = {
// Ignore already setup (race condition)
const currentId = player.media.getAttribute('id');
if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) {
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return;
}
@@ -151,18 +151,36 @@ const youtube = {
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (utils.is.empty(source)) {
if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}
// Replace the <iframe> with a <div> due to YouTube API issues
const videoId = utils.parseYouTubeId(source);
const id = utils.generateId(player.provider);
const container = utils.createElement('div', { id });
player.media = utils.replaceElement(container, player.media);
const videoId = parseId(source);
const id = generateId(player.provider);
// Set poster image
player.media.setAttribute('poster', utils.format(player.config.urls.youtube.poster, videoId));
// Get poster, if already set
const { poster } = player;
// Replace media element
const container = createElement('div', { id, poster });
player.media = replaceElement(container, player.media);
// Id to poster wrapper
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
.then(image => ui.setPoster.call(player, image.src))
.then(posterSrc => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!posterSrc.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
}
})
.catch(() => {});
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
@@ -188,49 +206,26 @@ const youtube = {
},
events: {
onError(event) {
// If we've already fired an error, don't do it again
// YouTube fires onError twice
if (utils.is.object(player.media.error)) {
return;
// YouTube may fire onError twice, so only handle it once
if (!player.media.error) {
const code = event.data;
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
const message =
{
2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
101: 'The owner of the requested video does not allow it to be played in embedded players.',
150: 'The owner of the requested video does not allow it to be played in embedded players.',
}[code] || 'An unknown error occured';
player.media.error = { code, message };
triggerEvent.call(player, player.media, 'error');
}
const detail = {
code: event.data,
};
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
switch (event.data) {
case 2:
detail.message =
'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.';
break;
case 5:
detail.message =
'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.';
break;
case 100:
detail.message =
'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.';
break;
case 101:
case 150:
detail.message = 'The owner of the requested video does not allow it to be played in embedded players.';
break;
default:
detail.message = 'An unknown error occured';
break;
}
player.media.error = detail;
utils.dispatchEvent.call(player, player.media, 'error');
},
onPlaybackQualityChange() {
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: player.media.quality,
});
},
@@ -241,7 +236,7 @@ const youtube = {
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
utils.dispatchEvent.call(player, player.media, 'ratechange');
triggerEvent.call(player, player.media, 'ratechange');
},
onReady(event) {
// Get the instance
@@ -252,10 +247,12 @@ const youtube = {
// Create a faux HTML5 API using the YouTube API
player.media.play = () => {
assurePlaybackState.call(player, true);
instance.playVideo();
};
player.media.pause = () => {
assurePlaybackState.call(player, false);
instance.pauseVideo();
};
@@ -273,22 +270,17 @@ const youtube = {
return Number(instance.getCurrentTime());
},
set(time) {
// Vimeo will automatically play on seek
const { paused } = player.media;
// If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
if (player.paused) {
player.embed.mute();
}
// Set seeking flag
// Set seeking state and trigger event
player.media.seeking = true;
// Trigger seeking
utils.dispatchEvent.call(player, player.media, 'seeking');
triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
// Restore pause state
if (paused) {
player.pause();
}
},
});
@@ -308,15 +300,7 @@ const youtube = {
return mapQualityUnit(instance.getPlaybackQuality());
},
set(input) {
const quality = input;
// Set via API
instance.setPlaybackQuality(mapQualityUnit(quality));
// Trigger request event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
quality,
});
instance.setPlaybackQuality(mapQualityUnit(input));
},
});
@@ -329,7 +313,7 @@ const youtube = {
set(input) {
volume = input;
instance.setVolume(volume * 100);
utils.dispatchEvent.call(player, player.media, 'volumechange');
triggerEvent.call(player, player.media, 'volumechange');
},
});
@@ -340,10 +324,10 @@ const youtube = {
return muted;
},
set(input) {
const toggle = utils.is.boolean(input) ? input : muted;
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
utils.dispatchEvent.call(player, player.media, 'volumechange');
triggerEvent.call(player, player.media, 'volumechange');
},
});
@@ -369,8 +353,8 @@ const youtube = {
player.media.setAttribute('tabindex', -1);
}
utils.dispatchEvent.call(player, player.media, 'timeupdate');
utils.dispatchEvent.call(player, player.media, 'durationchange');
triggerEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'durationchange');
// Reset timer
clearInterval(player.timers.buffering);
@@ -382,7 +366,7 @@ const youtube = {
// Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
utils.dispatchEvent.call(player, player.media, 'progress');
triggerEvent.call(player, player.media, 'progress');
}
// Set last buffer point
@@ -393,7 +377,7 @@ const youtube = {
clearInterval(player.timers.buffering);
// Trigger event
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
triggerEvent.call(player, player.media, 'canplaythrough');
}
}, 200);
@@ -407,6 +391,14 @@ const youtube = {
// Reset timer
clearInterval(player.timers.playing);
const seeked = player.media.seeking && [1, 2].includes(event.data);
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
}
// Handle events
// -1 Unstarted
// 0 Ended
@@ -417,16 +409,16 @@ const youtube = {
switch (event.data) {
case -1:
// Update scrubber
utils.dispatchEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
utils.dispatchEvent.call(player, player.media, 'progress');
triggerEvent.call(player, player.media, 'progress');
break;
case 0:
player.media.paused = true;
assurePlaybackState.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) {
@@ -434,48 +426,48 @@ const youtube = {
instance.stopVideo();
instance.playVideo();
} else {
utils.dispatchEvent.call(player, player.media, 'ended');
triggerEvent.call(player, player.media, 'ended');
}
break;
case 1:
// If we were seeking, fire seeked event
if (player.media.seeking) {
utils.dispatchEvent.call(player, player.media, 'seeked');
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (player.media.paused && !player.embed.hasPlayed) {
player.media.pause();
} else {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
triggerEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
// https://github.com/sampotts/plyr/issues/374
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
triggerEvent.call(player, player.media, 'durationchange');
}
// Get quality
controls.setQualityMenu.call(
player,
mapQualityUnits(instance.getAvailableQualityLevels()),
);
}
player.media.seeking = false;
// Only fire play if paused before
if (player.media.paused) {
utils.dispatchEvent.call(player, player.media, 'play');
}
player.media.paused = false;
utils.dispatchEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
utils.dispatchEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
// https://github.com/sampotts/plyr/issues/374
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
utils.dispatchEvent.call(player, player.media, 'durationchange');
}
// Get quality
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
break;
case 2:
player.media.paused = true;
utils.dispatchEvent.call(player, player.media, 'pause');
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.muted) {
player.embed.unMute();
}
assurePlaybackState.call(player, false);
break;
@@ -483,7 +475,7 @@ const youtube = {
break;
}
utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, {
triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},
+195 -328
View File
@@ -1,14 +1,15 @@
// ==========================================================================
// Plyr
// plyr.js v3.3.0
// plyr.js v3.3.13
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import captions from './captions';
import defaults from './config/defaults';
import { getProviderByUrl, providers, types } from './config/types';
import Console from './console';
import controls from './controls';
import defaults from './defaults';
import Fullscreen from './fullscreen';
import Listeners from './listeners';
import media from './media';
@@ -16,9 +17,14 @@ import Ads from './plugins/ads';
import source from './source';
import Storage from './storage';
import support from './support';
import { providers, types } from './types';
import ui from './ui';
import utils from './utils';
import { closest } from './utils/arrays';
import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';
import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
import is from './utils/is';
import loadSprite from './utils/loadSprite';
import { cloneDeep, extend } from './utils/objects';
import { parseUrl } from './utils/urls';
// Private properties
// TODO: Use a WeakMap for private globals
@@ -41,20 +47,21 @@ class Plyr {
this.media = target;
// String selector passed
if (utils.is.string(this.media)) {
if (is.string(this.media)) {
this.media = document.querySelectorAll(this.media);
}
// jQuery, NodeList or Array passed, use first element
if ((window.jQuery && this.media instanceof jQuery) || utils.is.nodeList(this.media) || utils.is.array(this.media)) {
if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) {
// eslint-disable-next-line
this.media = this.media[0];
}
// Set config
this.config = utils.extend(
this.config = extend(
{},
defaults,
Plyr.defaults,
options || {},
(() => {
try {
@@ -83,7 +90,8 @@ class Plyr {
// Captions
this.captions = {
active: null,
currentTrack: null,
currentTrack: -1,
meta: new WeakMap(),
};
// Fullscreen
@@ -95,7 +103,6 @@ class Plyr {
this.options = {
speed: [],
quality: [],
captions: [],
};
// Debugging
@@ -107,7 +114,7 @@ class Plyr {
this.debug.log('Support', support);
// We need an element to setup
if (utils.is.nullOrUndefined(this.media) || !utils.is.element(this.media)) {
if (is.nullOrUndefined(this.media) || !is.element(this.media)) {
this.debug.error('Setup failed: no suitable element passed');
return;
}
@@ -143,7 +150,6 @@ class Plyr {
// Embed properties
let iframe = null;
let url = null;
let params = null;
// Different setup based on type
switch (type) {
@@ -152,10 +158,10 @@ class Plyr {
iframe = this.media.querySelector('iframe');
// <iframe> type
if (utils.is.element(iframe)) {
if (is.element(iframe)) {
// Detect provider
url = iframe.getAttribute('src');
this.provider = utils.getProviderByUrl(url);
url = parseUrl(iframe.getAttribute('src'));
this.provider = getProviderByUrl(url.toString());
// Rework elements
this.elements.container = this.media;
@@ -165,24 +171,20 @@ class Plyr {
this.elements.container.className = '';
// Get attributes from URL and set config
params = utils.getUrlParams(url);
if (!utils.is.empty(params)) {
const truthy = [
'1',
'true',
];
if (url.searchParams.length) {
const truthy = ['1', 'true'];
if (truthy.includes(params.autoplay)) {
if (truthy.includes(url.searchParams.get('autoplay'))) {
this.config.autoplay = true;
}
if (truthy.includes(params.loop)) {
if (truthy.includes(url.searchParams.get('loop'))) {
this.config.loop.active = true;
}
// TODO: replace fullscreen.iosNative with this playsinline config option
// YouTube requires the playsinline in the URL
if (this.isYouTube) {
this.config.playsinline = truthy.includes(params.playsinline);
this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));
} else {
this.config.playsinline = true;
}
@@ -196,7 +198,7 @@ class Plyr {
}
// Unsupported or missing provider
if (utils.is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
if (is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
this.debug.error('Setup failed: Invalid provider');
return;
}
@@ -244,6 +246,8 @@ class Plyr {
return;
}
this.eventListeners = [];
// Create listeners
this.listeners = new Listeners(this);
@@ -254,14 +258,11 @@ class Plyr {
this.media.plyr = this;
// Wrap media
if (!utils.is.element(this.elements.container)) {
this.elements.container = utils.createElement('div');
utils.wrap(this.media, this.elements.container);
if (!is.element(this.elements.container)) {
this.elements.container = createElement('div');
wrap(this.media, this.elements.container);
}
// Allow focus to be captured
this.elements.container.setAttribute('tabindex', 0);
// Add style hook
ui.addStyleHook.call(this);
@@ -270,7 +271,7 @@ class Plyr {
// Listen for events if debugging
if (this.config.debug) {
utils.on(this.elements.container, this.config.events.join(' '), event => {
on.call(this, this.elements.container, this.config.events.join(' '), event => {
this.debug.log(`event: ${event.type}`);
});
}
@@ -329,15 +330,10 @@ class Plyr {
* Play the media, or play the advertisement (if they are not blocked)
*/
play() {
if (!utils.is.function(this.media.play)) {
if (!is.function(this.media.play)) {
return null;
}
// If ads are enabled, wait for them first
/* if (this.ads.enabled && !this.ads.initialized) {
return this.ads.managerPromise.then(() => this.ads.play()).catch(() => this.media.play());
} */
// Return the promise (for HTML5)
return this.media.play();
}
@@ -346,7 +342,7 @@ class Plyr {
* Pause the media
*/
pause() {
if (!this.playing || !utils.is.function(this.media.pause)) {
if (!this.playing || !is.function(this.media.pause)) {
return;
}
@@ -387,7 +383,7 @@ class Plyr {
*/
togglePlay(input) {
// Toggle based on current state if nothing passed
const toggle = utils.is.boolean(input) ? input : !this.playing;
const toggle = is.boolean(input) ? input : !this.playing;
if (toggle) {
this.play();
@@ -401,8 +397,9 @@ class Plyr {
*/
stop() {
if (this.isHTML5) {
this.media.load();
} else if (utils.is.function(this.media.stop)) {
this.pause();
this.restart();
} else if (is.function(this.media.stop)) {
this.media.stop();
}
}
@@ -419,7 +416,7 @@ class Plyr {
* @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
*/
rewind(seekTime) {
this.currentTime = this.currentTime - (utils.is.number(seekTime) ? seekTime : this.config.seekTime);
this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime);
}
/**
@@ -427,7 +424,7 @@ class Plyr {
* @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
*/
forward(seekTime) {
this.currentTime = this.currentTime + (utils.is.number(seekTime) ? seekTime : this.config.seekTime);
this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime);
}
/**
@@ -435,21 +432,16 @@ class Plyr {
* @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
*/
set currentTime(input) {
let targetTime = 0;
if (utils.is.number(input)) {
targetTime = input;
// Bail if media duration isn't available yet
if (!this.duration) {
return;
}
// Normalise targetTime
if (targetTime < 0) {
targetTime = 0;
} else if (targetTime > this.duration) {
targetTime = this.duration;
}
// Validate input
const inputIsValid = is.number(input) && input > 0;
// Set
this.media.currentTime = targetTime;
this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
// Logging
this.debug.log(`Seeking to ${this.currentTime} seconds`);
@@ -469,7 +461,7 @@ class Plyr {
const { buffered } = this.media;
// YouTube / Vimeo return a float between 0-1
if (utils.is.number(buffered)) {
if (is.number(buffered)) {
return buffered;
}
@@ -497,11 +489,11 @@ class Plyr {
// Faux duration set via config
const fauxDuration = parseFloat(this.config.duration);
// True duration
const realDuration = this.media ? Number(this.media.duration) : 0;
// Media duration can be NaN before the media has loaded
const duration = (this.media || {}).duration || 0;
// If custom duration is funky, use regular duration
return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration;
// If config duration is funky, use regular duration
return fauxDuration || duration;
}
/**
@@ -513,17 +505,17 @@ class Plyr {
const max = 1;
const min = 0;
if (utils.is.string(volume)) {
if (is.string(volume)) {
volume = Number(volume);
}
// Load volume from storage if no value specified
if (!utils.is.number(volume)) {
if (!is.number(volume)) {
volume = this.storage.get('volume');
}
// Use config if all else fails
if (!utils.is.number(volume)) {
if (!is.number(volume)) {
({ volume } = this.config);
}
@@ -543,7 +535,7 @@ class Plyr {
this.media.volume = volume;
// If muted, and we're increasing volume manually, reset muted state
if (!utils.is.empty(value) && this.muted && volume > 0) {
if (!is.empty(value) && this.muted && volume > 0) {
this.muted = false;
}
}
@@ -561,7 +553,7 @@ class Plyr {
*/
increaseVolume(step) {
const volume = this.media.muted ? 0 : this.volume;
this.volume = volume + (utils.is.number(step) ? step : 1);
this.volume = volume + (is.number(step) ? step : 1);
}
/**
@@ -570,7 +562,7 @@ class Plyr {
*/
decreaseVolume(step) {
const volume = this.media.muted ? 0 : this.volume;
this.volume = volume - (utils.is.number(step) ? step : 1);
this.volume = volume - (is.number(step) ? step : 1);
}
/**
@@ -581,12 +573,12 @@ class Plyr {
let toggle = mute;
// Load muted state from storage
if (!utils.is.boolean(toggle)) {
if (!is.boolean(toggle)) {
toggle = this.storage.get('muted');
}
// Use config if all else fails
if (!utils.is.boolean(toggle)) {
if (!is.boolean(toggle)) {
toggle = this.config.muted;
}
@@ -632,15 +624,15 @@ class Plyr {
set speed(input) {
let speed = null;
if (utils.is.number(input)) {
if (is.number(input)) {
speed = input;
}
if (!utils.is.number(speed)) {
if (!is.number(speed)) {
speed = this.storage.get('speed');
}
if (!utils.is.number(speed)) {
if (!is.number(speed)) {
speed = this.config.speed.selected;
}
@@ -677,36 +669,31 @@ class Plyr {
* @param {number} input - Quality level
*/
set quality(input) {
let quality = null;
const config = this.config.quality;
const options = this.options.quality;
if (!utils.is.empty(input)) {
quality = Number(input);
}
if (!utils.is.number(quality) || quality === 0) {
quality = this.storage.get('quality');
}
if (!utils.is.number(quality)) {
quality = this.config.quality.selected;
}
if (!utils.is.number(quality)) {
quality = this.config.quality.default;
}
if (!this.options.quality.length) {
if (!options.length) {
return;
}
if (!this.options.quality.includes(quality)) {
const closest = utils.closest(this.options.quality, quality);
this.debug.warn(`Unsupported quality option: ${quality}, using ${closest} instead`);
quality = closest;
let quality = [
!is.empty(input) && Number(input),
this.storage.get('quality'),
config.selected,
config.default,
].find(is.number);
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
this.config.quality.selected = quality;
config.selected = quality;
// Set quality
this.media.quality = quality;
@@ -725,7 +712,7 @@ class Plyr {
* @param {boolean} input - Whether to loop or not
*/
set loop(input) {
const toggle = utils.is.boolean(input) ? input : this.config.loop.active;
const toggle = is.boolean(input) ? input : this.config.loop.active;
this.config.loop.active = toggle;
this.media.loop = toggle;
@@ -805,10 +792,7 @@ class Plyr {
return;
}
if (utils.is.string(input)) {
this.media.setAttribute('poster', input);
ui.setPoster.call(this);
}
ui.setPoster.call(this, input, false).catch(() => {});
}
/**
@@ -827,7 +811,7 @@ class Plyr {
* @param {boolean} input - Whether to autoplay or not
*/
set autoplay(input) {
const toggle = utils.is.boolean(input) ? input : this.config.autoplay;
const toggle = is.boolean(input) ? input : this.config.autoplay;
this.config.autoplay = toggle;
}
@@ -843,88 +827,39 @@ class Plyr {
* @param {boolean} input - Whether to enable captions
*/
toggleCaptions(input) {
// If there's no full support
if (!this.supported.ui) {
return;
}
// If the method is called without parameter, toggle based on current value
const show = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
// Nothing to change...
if (this.captions.active === show) {
return;
}
// Set global
this.captions.active = show;
// Toggle state
utils.toggleState(this.elements.buttons.captions, this.captions.active);
// Add class hook
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, this.captions.active);
// Trigger an event
utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
captions.toggle.call(this, input, false);
}
/**
* 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)
*/
set language(input) {
// Nothing specified
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');
captions.setLanguage.call(this, input, false);
}
/**
* Get the current captions language
* Get the current track's language
*/
get language() {
return this.captions.language;
return (captions.getCurrentTrack.call(this) || {}).language;
}
/**
@@ -944,7 +879,7 @@ class Plyr {
}
// 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
this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline);
@@ -974,119 +909,32 @@ class Plyr {
/**
* Toggle the player controls
* @param {boolean} toggle - Whether to show the controls
* @param {boolean} [toggle] - Whether to show the controls
*/
toggleControls(toggle) {
// We need controls of course...
if (!utils.is.element(this.elements.controls)) {
return;
}
// Don't toggle if missing UI support or if it's audio
if (this.supported.ui && !this.isAudio) {
// Get state before change
const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);
// Don't hide if no UI support or it's audio
if (!this.supported.ui || this.isAudio) {
return;
}
// Negate the argument if not undefined since adding the class to hides the controls
const force = typeof toggle === 'undefined' ? undefined : !toggle;
let delay = 0;
let show = toggle;
let isEnterFullscreen = false;
// Apply and get updated state
const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);
// Get toggle state if not set
if (!utils.is.boolean(toggle)) {
if (utils.is.event(toggle)) {
// Is the enter fullscreen event
isEnterFullscreen = toggle.type === 'enterfullscreen';
// Events that show the controls
const showEvents = [
'touchstart',
'touchmove',
'mouseenter',
'mousemove',
'focusin',
];
// Events that delay hiding
const delayEvents = [
'touchmove',
'touchend',
'mousemove',
];
// Whether to show controls
show = showEvents.includes(toggle.type);
// Delay hiding on move events
if (delayEvents.includes(toggle.type)) {
delay = 2000;
}
// Delay a little more for keyboard users
if (!this.touch && toggle.type === 'focusin') {
delay = 3000;
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true);
}
} else {
show = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
// Close menu
if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
controls.toggleMenu.call(this, false);
}
}
// Clear timer on every call
clearTimeout(this.timers.controls);
// If the mouse is not over the controls, set a timeout to hide them
if (show || this.paused || this.loading) {
// Check if controls toggled
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, false);
// Trigger event
if (toggled) {
utils.dispatchEvent.call(this, this.media, 'controlsshown');
}
// Always show controls when paused or if touch
if (this.paused || this.loading) {
return;
}
// Delay for hiding on touch
if (this.touch) {
delay = 3000;
// Trigger event on change
if (hiding !== isHidden) {
const eventName = hiding ? 'controlshidden' : 'controlsshown';
triggerEvent.call(this, this.media, eventName);
}
return !hiding;
}
// If toggle is false or if we're playing (regardless of toggle),
// then set the timer to hide the controls
if (!show || this.playing) {
this.timers.controls = setTimeout(() => {
// We need controls of course...
if (!utils.is.element(this.elements.controls)) {
return;
}
// If the mouse is over the controls (and not entering fullscreen), bail
if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) {
return;
}
// Restore transition behaviour
if (!utils.hasClass(this.elements.container, this.config.classNames.hideControls)) {
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false);
}
// Set hideControls class
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, this.config.hideControls);
// Trigger event and close menu
if (toggled) {
utils.dispatchEvent.call(this, this.media, 'controlshidden');
if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
controls.toggleMenu.call(this, false);
}
}
}, delay);
}
return false;
}
/**
@@ -1095,16 +943,23 @@ class Plyr {
* @param {function} callback - Callback for when event occurs
*/
on(event, callback) {
utils.on(this.elements.container, event, callback);
on.call(this, this.elements.container, event, callback);
}
/**
* Add event listeners once
* @param {string} event - Event type
* @param {function} callback - Callback for when event occurs
*/
once(event, callback) {
once.call(this, this.elements.container, event, callback);
}
/**
* Remove event listeners
* @param {string} event - Event type
* @param {function} callback - Callback for when event occurs
*/
off(event, callback) {
utils.off(this.elements.container, event, callback);
off(this.elements.container, event, callback);
}
/**
@@ -1130,10 +985,10 @@ class Plyr {
if (soft) {
if (Object.keys(this.elements).length) {
// Remove elements
utils.removeElement(this.elements.buttons.play);
utils.removeElement(this.elements.captions);
utils.removeElement(this.elements.controls);
utils.removeElement(this.elements.wrapper);
removeElement(this.elements.buttons.play);
removeElement(this.elements.captions);
removeElement(this.elements.controls);
removeElement(this.elements.wrapper);
// Clear for GC
this.elements.buttons.play = null;
@@ -1143,21 +998,21 @@ class Plyr {
}
// Callback
if (utils.is.function(callback)) {
if (is.function(callback)) {
callback();
}
} else {
// Unbind listeners
this.listeners.clear();
unbindListeners.call(this);
// Replace the container with the original element provided
utils.replaceElement(this.elements.original, this.elements.container);
replaceElement(this.elements.original, this.elements.container);
// Event
utils.dispatchEvent.call(this, this.elements.original, 'destroyed', true);
triggerEvent.call(this, this.elements.original, 'destroyed', true);
// Callback
if (utils.is.function(callback)) {
if (is.function(callback)) {
callback.call(this.elements.original);
}
@@ -1175,50 +1030,37 @@ class Plyr {
// Stop playback
this.stop();
// Type specific stuff
switch (`${this.provider}:${this.type}`) {
case 'html5:video':
case 'html5:audio':
// Clear timeout
clearTimeout(this.timers.loading);
// Provider specific stuff
if (this.isHTML5) {
// Clear timeout
clearTimeout(this.timers.loading);
// Restore native video controls
ui.toggleNativeControls.call(this, true);
// Restore native video controls
ui.toggleNativeControls.call(this, true);
// Clean up
done();
// Clean up
done();
} else if (this.isYouTube) {
// Clear timers
clearInterval(this.timers.buffering);
clearInterval(this.timers.playing);
break;
// Destroy YouTube API
if (this.embed !== null && is.function(this.embed.destroy)) {
this.embed.destroy();
}
case 'youtube:video':
// Clear timers
clearInterval(this.timers.buffering);
clearInterval(this.timers.playing);
// Clean up
done();
} else if (this.isVimeo) {
// Destroy Vimeo API
// then clean up (wait, to prevent postmessage errors)
if (this.embed !== null) {
this.embed.unload().then(done);
}
// Destroy YouTube API
if (this.embed !== null && utils.is.function(this.embed.destroy)) {
this.embed.destroy();
}
// Clean up
done();
break;
case 'vimeo:video':
// Destroy Vimeo API
// then clean up (wait, to prevent postmessage errors)
if (this.embed !== null) {
this.embed.unload().then(done);
}
// Vimeo does not always return
setTimeout(done, 200);
break;
default:
break;
// Vimeo does not always return
setTimeout(done, 200);
}
}
@@ -1246,8 +1088,33 @@ class Plyr {
* @param {string} [id] - Unique ID
*/
static loadSprite(url, id) {
return utils.loadSprite(url, id);
return loadSprite(url, id);
}
/**
* Setup multiple instances
* @param {*} selector
* @param {object} options
*/
static setup(selector, options = {}) {
let targets = null;
if (is.string(selector)) {
targets = Array.from(document.querySelectorAll(selector));
} else if (is.nodeList(selector)) {
targets = Array.from(selector);
} else if (is.array(selector)) {
targets = selector.filter(is.element);
}
if (is.empty(targets)) {
return null;
}
return targets.map(t => new Plyr(t, options));
}
}
Plyr.defaults = cloneDeep(defaults);
export default Plyr;
+2 -1
View File
@@ -1,12 +1,13 @@
// ==========================================================================
// Plyr Polyfilled Build
// plyr.js v3.3.0
// plyr.js v3.3.13
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import 'babel-polyfill';
import 'custom-event-polyfill';
import 'url-polyfill';
import Plyr from './plyr';
export default Plyr;
+28 -39
View File
@@ -2,23 +2,25 @@
// Plyr source update
// ==========================================================================
import { providers } from './types';
import utils from './utils';
import { providers } from './config/types';
import html5 from './html5';
import media from './media';
import ui from './ui';
import support from './support';
import ui from './ui';
import { createElement, insertElement, removeElement } from './utils/elements';
import is from './utils/is';
import { getDeep } from './utils/objects';
const source = {
// Add elements to HTML5 media (source, tracks, etc)
insertElements(type, attributes) {
if (utils.is.string(attributes)) {
utils.insertElement(type, this.media, {
if (is.string(attributes)) {
insertElement(type, this.media, {
src: attributes,
});
} else if (utils.is.array(attributes)) {
} else if (is.array(attributes)) {
attributes.forEach(attribute => {
utils.insertElement(type, this.media, attribute);
insertElement(type, this.media, attribute);
});
}
},
@@ -26,7 +28,7 @@ const source = {
// Update source
// Sources are not checked for support so be careful
change(input) {
if (!utils.is.object(input) || !('sources' in input) || !input.sources.length) {
if (!getDeep(input, 'sources.length')) {
this.debug.warn('Invalid source format');
return;
}
@@ -42,47 +44,34 @@ const source = {
this.options.quality = [];
// Remove elements
utils.removeElement(this.media);
removeElement(this.media);
this.media = null;
// Reset class name
if (utils.is.element(this.elements.container)) {
if (is.element(this.elements.container)) {
this.elements.container.removeAttribute('class');
}
// Set the type and provider
this.type = input.type;
this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5;
const { sources, type } = input;
const [{ provider = providers.html5, src }] = sources;
const tagName = provider === 'html5' ? type : 'div';
const attributes = provider === 'html5' ? {} : { src };
// Check for support
this.supported = support.check(this.type, this.provider, this.config.playsinline);
// Create new markup
switch (`${this.provider}:${this.type}`) {
case 'html5:video':
this.media = utils.createElement('video');
break;
case 'html5:audio':
this.media = utils.createElement('audio');
break;
case 'youtube:video':
case 'vimeo:video':
this.media = utils.createElement('div', {
src: input.sources[0].src,
});
break;
default:
break;
}
Object.assign(this, {
provider,
type,
// Check for support
supported: support.check(type, provider, this.config.playsinline),
// Create new element
media: createElement(tagName, attributes),
});
// Inject the new element
this.elements.container.appendChild(this.media);
// Autoplay the new source?
if (utils.is.boolean(input.autoplay)) {
if (is.boolean(input.autoplay)) {
this.config.autoplay = input.autoplay;
}
@@ -94,8 +83,8 @@ const source = {
if (this.config.autoplay) {
this.media.setAttribute('autoplay', '');
}
if ('poster' in input) {
this.media.setAttribute('poster', input.poster);
if (!is.empty(input.poster)) {
this.poster = input.poster;
}
if (this.config.loop.active) {
this.media.setAttribute('loop', '');
@@ -113,7 +102,7 @@ const source = {
// Set new sources for html5
if (this.isHTML5) {
source.insertElements.call(this, 'source', input.sources);
source.insertElements.call(this, 'source', sources);
}
// Set video title
+8 -7
View File
@@ -2,7 +2,8 @@
// Plyr storage
// ==========================================================================
import utils from './utils';
import is from './utils/is';
import { extend } from './utils/objects';
class Storage {
constructor(player) {
@@ -31,19 +32,19 @@ class Storage {
}
get(key) {
if (!Storage.supported) {
if (!Storage.supported || !this.enabled) {
return null;
}
const store = window.localStorage.getItem(this.key);
if (utils.is.empty(store)) {
if (is.empty(store)) {
return null;
}
const json = JSON.parse(store);
return utils.is.string(key) && key.length ? json[key] : json;
return is.string(key) && key.length ? json[key] : json;
}
set(object) {
@@ -53,7 +54,7 @@ class Storage {
}
// Can only store objectst
if (!utils.is.object(object)) {
if (!is.object(object)) {
return;
}
@@ -61,12 +62,12 @@ class Storage {
let storage = this.get();
// Default to empty object
if (utils.is.empty(storage)) {
if (is.empty(storage)) {
storage = {};
}
// Update the working copy of the values
utils.extend(storage, object);
extend(storage, object);
// Update storage
window.localStorage.setItem(this.key, JSON.stringify(storage));
+38 -100
View File
@@ -2,7 +2,19 @@
// Plyr support checks
// ==========================================================================
import utils from './utils';
import { transitionEndEvent } from './utils/animation';
import browser from './utils/browser';
import { createElement } from './utils/elements';
import is from './utils/is';
// Default codecs for checking mimetype support
const defaultCodecs = {
'audio/ogg': 'vorbis',
'audio/wav': '1',
'video/webm': 'vp8, vorbis',
'video/mp4': 'avc1.42E01E, mp4a.40.2',
'video/ogg': 'theora',
};
// Check for feature support
const support = {
@@ -13,32 +25,9 @@ const support = {
// Check for support
// Basic functionality vs full UI
check(type, provider, playsinline) {
let api = false;
let ui = false;
const browser = utils.getBrowser();
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
switch (`${provider}:${type}`) {
case 'html5:video':
api = support.video;
ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline);
break;
case 'html5:audio':
api = support.audio;
ui = api && support.rangeInput;
break;
case 'youtube:video':
case 'vimeo:video':
api = true;
ui = support.rangeInput && (!browser.isIPhone || canPlayInline);
break;
default:
api = support.audio && support.video;
ui = api && support.rangeInput;
}
const api = support[type] || provider !== 'html5';
const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
return {
api,
@@ -48,14 +37,11 @@ const support = {
// Picture-in-picture support
// Safari only currently
pip: (() => {
const browser = utils.getBrowser();
return !browser.isIPhone && utils.is.function(utils.createElement('video').webkitSetPresentationMode);
})(),
pip: (() => !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode))(),
// Airplay support
// Safari only currently
airplay: utils.is.function(window.WebKitPlaybackTargetAvailabilityEvent),
airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
// Inline playback support
// https://webkit.org/blog/6784/new-video-policies-for-ios/
@@ -64,82 +50,34 @@ const support = {
// Check for mime type support against a player instance
// Credits: http://diveintohtml5.info/everything.html
// Related: http://www.leanbackplayer.com/test/h5mt.html
mime(type) {
const { media } = this;
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) {
mime(inputType) {
const [mediaType] = inputType.split('/');
if (!this.isHTML5 || mediaType !== this.type) {
return false;
}
// If we got this far, we're stuffed
return false;
let type;
if (inputType && inputType.includes('codecs=')) {
// Use input directly
type = inputType;
} else if (inputType === 'audio/mpeg') {
// Skip codec
type = 'audio/mpeg;';
} else if (inputType in defaultCodecs) {
// Use codec
type = `${inputType}; codecs="${defaultCodecs[inputType]}"`;
}
try {
return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
} catch (err) {
return false;
}
},
// Check for textTracks support
textTracks: 'textTracks' in document.createElement('video'),
// Check for passive event listener support
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
// https://www.youtube.com/watch?v=NPM6172J22g
passiveListeners: (() => {
// Test via a getter in the options object to see if the passive property is accessed
let supported = false;
try {
const options = Object.defineProperty({}, 'passive', {
get() {
supported = true;
return null;
},
});
window.addEventListener('test', null, options);
} catch (e) {
// Do nothing
}
return supported;
})(),
// <input type="range"> Sliders
rangeInput: (() => {
const range = document.createElement('input');
@@ -152,7 +90,7 @@ const support = {
touch: 'ontouchstart' in document.documentElement,
// Detect transitions support
transitions: utils.transitionEndEvent !== false,
transitions: transitionEndEvent !== false,
// Reduced motion iOS & MacOS setting
// https://webkit.org/blog/7551/responsive-design-for-motion/
+107 -203
View File
@@ -6,15 +6,16 @@ import captions from './captions';
import controls from './controls';
import i18n from './i18n';
import support from './support';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
import browser from './utils/browser';
import { getElement, toggleClass } from './utils/elements';
import { ready, triggerEvent } from './utils/events';
import is from './utils/is';
import loadImage from './utils/loadImage';
const ui = {
addStyleHook() {
utils.toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
utils.toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
},
// Toggle native HTML5 media controls
@@ -44,7 +45,7 @@ const ui = {
}
// Inject custom controls if not present
if (!utils.is.element(this.elements.controls)) {
if (!is.element(this.elements.controls)) {
// Inject custom controls
controls.inject.call(this);
@@ -55,8 +56,10 @@ const ui = {
// Remove native controls
ui.toggleNativeControls.call(this);
// Captions
captions.setup.call(this);
// Setup captions for HTML5
if (this.isHTML5) {
captions.setup.call(this);
}
// Reset volume
this.volume = null;
@@ -74,39 +77,51 @@ const ui = {
this.quality = null;
// Reset volume display
ui.updateVolume.call(this);
controls.updateVolume.call(this);
// Reset time display
ui.timeUpdate.call(this);
controls.timeUpdate.call(this);
// Update the UI
ui.checkPlaying.call(this);
// Check for picture-in-picture support
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
toggleClass(
this.elements.container,
this.config.classNames.pip.supported,
support.pip && this.isHTML5 && this.isVideo,
);
// Check for airplay support
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
// Ready for API calls
this.ready = true;
// Ready event at end of execution stack
setTimeout(() => {
utils.dispatchEvent.call(this, this.media, 'ready');
triggerEvent.call(this, this.media, 'ready');
}, 0);
// Set the title
ui.setTitle.call(this);
// Set the poster image
ui.setPoster.call(this);
// Assure the poster image is set, if the property was added before the element was created
if (this.poster) {
ui.setPoster.call(this, this.poster, false).catch(() => {});
}
// Manually set the duration if user has overridden it.
// The event listeners for it doesn't get called if preload is disabled (#701)
if (this.config.duration) {
controls.durationUpdate.call(this);
}
},
// Setup aria attribute for play and iframe title
@@ -115,15 +130,12 @@ const ui = {
let label = i18n.get('play', this.config);
// If there's a media title set, use that for the label
if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) {
if (is.string(this.config.title) && !is.empty(this.config.title)) {
label += `, ${this.config.title}`;
// Set container label
this.elements.container.setAttribute('aria-label', this.config.title);
}
// If there's a play button, set label
if (utils.is.nodeList(this.elements.buttons.play)) {
if (is.nodeList(this.elements.buttons.play)) {
Array.from(this.elements.buttons.play).forEach(button => {
button.setAttribute('aria-label', label);
});
@@ -132,222 +144,114 @@ const ui = {
// Set iframe title
// https://github.com/sampotts/plyr/issues/124
if (this.isEmbed) {
const iframe = utils.getElement.call(this, 'iframe');
const iframe = getElement.call(this, 'iframe');
if (!utils.is.element(iframe)) {
if (!is.element(iframe)) {
return;
}
// Default to media type
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
const title = !is.empty(this.config.title) ? this.config.title : 'video';
const format = i18n.get('frameTitle', this.config);
iframe.setAttribute('title', format.replace('{title}', title));
}
},
// Set the poster image
setPoster() {
if (!utils.is.element(this.elements.poster) || utils.is.empty(this.poster)) {
return;
// Toggle poster
togglePoster(enable) {
toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
},
// Set the poster image (async)
// Used internally for the poster setter, with the passive option forced to false
setPoster(poster, passive = true) {
// Don't override if call is passive
if (passive && this.poster) {
return Promise.reject(new Error('Poster already set'));
}
// Set the inline style
const posters = this.poster.split(',');
this.elements.poster.style.backgroundImage = posters.map(p => `url('${p}')`).join(',');
// Set property synchronously to respect the call order
this.media.setAttribute('poster', poster);
// Wait until ui is ready
return (
ready
.call(this)
// Load image
.then(() => loadImage(poster))
.catch(err => {
// Hide poster on error unless it's been set by another call
if (poster === this.poster) {
ui.togglePoster.call(this, false);
}
// Rethrow
throw err;
})
.then(() => {
// Prevent race conditions
if (poster !== this.poster) {
throw new Error('setPoster cancelled by later call to setPoster');
}
})
.then(() => {
Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`,
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
ui.togglePoster.call(this, true);
return poster;
})
);
},
// Check playing state
checkPlaying() {
checkPlaying(event) {
// Class hooks
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set ARIA state
utils.toggleState(this.elements.buttons.play, this.playing);
// Set state
Array.from(this.elements.buttons.play).forEach(target => {
target.pressed = this.playing;
});
// Only update controls on non timeupdate events
if (is.event(event) && event.type === 'timeupdate') {
return;
}
// Toggle controls
this.toggleControls(!this.playing);
ui.toggleControls.call(this);
},
// Check if media is loading
checkLoading(event) {
this.loading = [
'stalled',
'waiting',
].includes(event.type);
this.loading = ['stalled', 'waiting'].includes(event.type);
// Clear timer
clearTimeout(this.timers.loading);
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => {
// Toggle container class hook
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Update progress bar loading class state
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Show controls if loading, hide if done
this.toggleControls(this.loading);
// Update controls visibility
ui.toggleControls.call(this);
}, this.loading ? 250 : 0);
},
// Check if media failed to load
checkFailed() {
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState
this.failed = this.media.networkState === 3;
// Toggle controls based on state and `force` argument
toggleControls(force) {
const { controls } = this.elements;
if (this.failed) {
utils.toggleClass(this.elements.container, this.config.classNames.loading, false);
utils.toggleClass(this.elements.container, this.config.classNames.error, true);
if (controls && this.config.hideControls) {
// Show controls if force, loading, paused, or button interaction, otherwise hide
this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover));
}
// Clear timer
clearTimeout(this.timers.failed);
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => {
// Toggle container class hook
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Show controls if loading, hide if done
this.toggleControls(this.loading);
}, this.loading ? 250 : 0);
},
// Update volume UI and storage
updateVolume() {
if (!this.supported.ui) {
return;
}
// Update range
if (utils.is.element(this.elements.inputs.volume)) {
ui.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
}
// Update mute state
if (utils.is.element(this.elements.buttons.mute)) {
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
}
},
// Update seek value and lower fill
setRange(target, value = 0) {
if (!utils.is.element(target)) {
return;
}
// eslint-disable-next-line
target.value = value;
// Webkit range fill
controls.updateRangeFill.call(this, target);
},
// Set <progress> value
setProgress(target, input) {
const value = utils.is.number(input) ? input : 0;
const progress = utils.is.element(target) ? target : this.elements.display.buffer;
// Update value and label
if (utils.is.element(progress)) {
progress.value = value;
// Update text label inside
const label = progress.getElementsByTagName('span')[0];
if (utils.is.element(label)) {
label.childNodes[0].nodeValue = value;
}
}
},
// Update <progress> elements
updateProgress(event) {
if (!this.supported.ui || !utils.is.event(event)) {
return;
}
let value = 0;
if (event) {
switch (event.type) {
// Video playing
case 'timeupdate':
case 'seeking':
value = utils.getPercentage(this.currentTime, this.duration);
// Set seek range value only if it's a 'natural' time event
if (event.type === 'timeupdate') {
ui.setRange.call(this, this.elements.inputs.seek, value);
}
break;
// Check buffer status
case 'playing':
case 'progress':
ui.setProgress.call(this, this.elements.display.buffer, this.buffered * 100);
break;
default:
break;
}
}
},
// Update the displayed time
updateTimeDisplay(target = null, time = 0, inverted = false) {
// Bail if there's no element to display or the value isn't a number
if (!utils.is.element(target) || !utils.is.number(time)) {
return;
}
// Always display hours if duration is over an hour
const forceHours = utils.getHours(this.duration) > 0;
// eslint-disable-next-line no-param-reassign
target.textContent = utils.formatTime(time, forceHours, inverted);
},
// Handle time change event
timeUpdate(event) {
// Only invert if only one time element is displayed and used for both duration and currentTime
const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
// Duration
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
// Ignore updates while seeking
if (event && event.type === 'timeupdate' && this.media.seeking) {
return;
}
// Playing progress
ui.updateProgress.call(this, event);
},
// Show the duration on metadataloaded
durationUpdate() {
if (!this.supported.ui) {
return;
}
// If there's a spot to display duration
const hasDuration = utils.is.element(this.elements.display.duration);
// If there's only one time display, display duration there
if (!hasDuration && this.config.displayDuration && this.paused) {
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
}
// If there's a duration element, update content
if (hasDuration) {
ui.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
}
// Update the tooltip (if visible)
controls.updateSeekTooltip.call(this);
},
};
-842
View File
@@ -1,842 +0,0 @@
// ==========================================================================
// Plyr utils
// ==========================================================================
import loadjs from 'loadjs';
import support from './support';
import { providers } from './types';
const utils = {
// Check variable types
is: {
plyr(input) {
return this.instanceof(input, window.Plyr);
},
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, window.WeakMap);
},
nodeList(input) {
return this.instanceof(input, window.NodeList);
},
element(input) {
return this.instanceof(input, window.Element);
},
textNode(input) {
return this.getConstructor(input) === Text;
},
event(input) {
return this.instanceof(input, window.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 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()) {
// Create container
const container = document.createElement('div');
utils.toggleHidden(container, true);
if (hasId) {
container.setAttribute('id', id);
}
// Check in cache
if (support.storage) {
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 (support.storage) {
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.textContent = 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');
}
},
// Toggle class on an element
toggleClass(element, className, toggle) {
if (utils.is.element(element)) {
const contains = element.classList.contains(className);
element.classList[toggle ? 'add' : 'remove'](className);
return (toggle && !contains) || (!toggle && contains);
}
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: utils.is.plyr(this) ? this : null,
}),
});
// 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);
},
// 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{8,}(?=\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.statusText);
});
request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send();
} catch (e) {
reject(e);
}
});
}
+67
View File
@@ -0,0 +1,67 @@
// ==========================================================================
// 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 is = {
object(input) {
return getConstructor(input) === Object;
},
number(input) {
return getConstructor(input) === Number && !Number.isNaN(input);
},
string(input) {
return getConstructor(input) === String;
},
boolean(input) {
return getConstructor(input) === Boolean;
},
function(input) {
return getConstructor(input) === Function;
},
array(input) {
return !is.nullOrUndefined(input) && Array.isArray(input);
},
weakMap(input) {
return instanceOf(input, WeakMap);
},
nodeList(input) {
return instanceOf(input, NodeList);
},
element(input) {
return instanceOf(input, Element);
},
textNode(input) {
return getConstructor(input) === Text;
},
event(input) {
return instanceOf(input, Event);
},
cue(input) {
return instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
},
track(input) {
return instanceOf(input, TextTrack) || (!is.nullOrUndefined(input) && is.string(input.kind));
},
url(input) {
return (
!is.nullOrUndefined(input) &&
/(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input)
);
},
nullOrUndefined(input) {
return input === null || typeof input === 'undefined';
},
empty(input) {
return (
is.nullOrUndefined(input) ||
((is.string(input) || is.array(input) || is.nodeList(input)) && !input.length) ||
(is.object(input) && !Object.keys(input).length)
);
},
};
export default is;
+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,
});
});
}
+75
View File
@@ -0,0 +1,75 @@
// ==========================================================================
// Sprite loader
// ==========================================================================
import Storage from './../storage';
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;
width: 100%;
span {
.plyr__caption {
background: $plyr-captions-bg;
border-radius: 2px;
box-decoration-break: clone;
+4 -4
View File
@@ -34,10 +34,10 @@
}
// Change icons on state change
.plyr__control[aria-pressed='false'] .icon--pressed,
.plyr__control[aria-pressed='true'] .icon--not-pressed,
.plyr__control[aria-pressed='false'] .label--pressed,
.plyr__control[aria-pressed='true'] .label--not-pressed {
.plyr__control:not(.plyr__control--pressed) .icon--pressed,
.plyr__control.plyr__control--pressed .icon--not-pressed,
.plyr__control:not(.plyr__control--pressed) .label--pressed,
.plyr__control.plyr__control--pressed .label--not-pressed {
display: none;
}
+2 -2
View File
@@ -32,8 +32,8 @@ $embed-padding: ((100 / 16) * 9);
pointer-events: none;
}
// Vimeo hack
> div {
// Only used for Vimeo
> .plyr__video-embed__container {
padding-bottom: to-percentage($height);
position: relative;
transform: translateY(-$offset);
+3 -2
View File
@@ -6,7 +6,7 @@
background-color: #000;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: 100% 100%;
background-size: contain;
height: 100%;
left: 0;
opacity: 0;
@@ -15,8 +15,9 @@
transition: opacity 0.3s ease;
width: 100%;
z-index: 1;
pointer-events: none;
}
.plyr--stopped .plyr__poster {
.plyr--stopped.plyr__poster-enabled .plyr__poster {
opacity: 1;
}
+17 -8
View File
@@ -5,8 +5,18 @@
.plyr__progress {
display: flex;
flex: 1;
left: $plyr-range-thumb-height / 2;
margin-right: $plyr-range-thumb-height;
position: relative;
input[type='range'],
&__buffer {
margin-left: -($plyr-range-thumb-height / 2);
margin-right: -($plyr-range-thumb-height / 2);
// Offset the range thumb in order to be able to calculate the relative progress (#954)
width: calc(100% + #{$plyr-range-thumb-height});
}
input[type='range'] {
position: relative;
z-index: 2;
@@ -19,18 +29,17 @@
}
}
.plyr__progress--buffer {
.plyr__progress__buffer {
-webkit-appearance: none; /* stylelint-disable-line */
background: transparent;
border: 0;
border-radius: 100px;
height: $plyr-range-track-height;
left: 0;
margin: -($plyr-range-track-height / 2) 0 0;
margin-top: -($plyr-range-track-height / 2);
padding: 0;
position: absolute;
top: 50%;
width: 100%;
&::-webkit-progress-bar {
background: transparent;
@@ -58,17 +67,17 @@
}
}
.plyr--video .plyr__progress--buffer {
.plyr--video .plyr__progress__buffer {
box-shadow: 0 1px 1px rgba(#000, 0.15);
color: $plyr-video-progress-buffered-bg;
}
.plyr--audio .plyr__progress--buffer {
.plyr--audio .plyr__progress__buffer {
color: $plyr-audio-progress-buffered-bg;
}
// Loading state
.plyr--loading .plyr__progress--buffer {
.plyr--loading .plyr__progress__buffer {
animation: plyr-progress 1s linear infinite;
background-image: linear-gradient(
-45deg,
@@ -85,10 +94,10 @@
color: transparent;
}
.plyr--video.plyr--loading .plyr__progress--buffer {
.plyr--video.plyr--loading .plyr__progress__buffer {
background-color: $plyr-video-progress-buffered-bg;
}
.plyr--audio.plyr--loading .plyr__progress--buffer {
.plyr--audio.plyr--loading .plyr__progress__buffer {
background-color: $plyr-audio-progress-buffered-bg;
}
+1
View File
@@ -19,6 +19,7 @@
transform: translate(-50%, 10px) scale(0.8);
transform-origin: 50% 100%;
transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease;
white-space: nowrap;
z-index: 2;
// The background triangle
+2 -3
View File
@@ -31,15 +31,14 @@
@import 'components/controls';
@import 'components/embed';
@import 'components/menus';
@import 'components/progress';
@import 'components/poster';
@import 'components/sliders';
@import 'components/poster';
@import 'components/times';
@import 'components/tooltips';
@import 'components/video';
@import 'components/progress';
@import 'components/volume';
@import 'states/error';
@import 'states/fullscreen';
@import 'plugins/ads';
-25
View File
@@ -1,25 +0,0 @@
// --------------------------------------------------------------
// Error state
// --------------------------------------------------------------
.plyr--has-error {
pointer-events: none;
&::after {
align-items: center;
background: rgba(#000, 90%);
color: #fff;
content: attr(data-plyr-error);
display: flex;
font-size: $plyr-font-size-base;
height: 100%;
justify-content: center;
left: 0;
position: absolute;
text-align: center;
text-shadow: 0 1px 1px rgba(#000, 10%);
top: 0;
width: 100%;
z-index: 10;
}
}
+341 -1549
View File
File diff suppressed because it is too large Load Diff