Compare commits

...

520 Commits

Author SHA1 Message Date
7dc4d9cd22 v3.4.3 2018-08-14 12:16:34 +10:00
8fb8ae1260 Merge pull request #1163 from sampotts/develop
Fix bug with nodeList for play buttons
2018-08-14 12:14:58 +10:00
90304369f4 Fix watch 2018-08-14 12:13:16 +10:00
922456c46c Fix for nodeList as buttons 2018-08-14 12:13:00 +10:00
eaeccd66ae v3.4.2 2018-08-14 11:17:33 +10:00
7a43649c13 Fix play/pause button state 2018-08-14 11:17:27 +10:00
525bbf313e v3.4.1 2018-08-14 09:18:09 +10:00
cfaebe9bf2 Fix for controls missing (fixes #1161) 2018-08-14 09:17:58 +10:00
b57b7b2153 v3.4.0
-   Accessibility improvements (see #905)
-   Improvements to the way the controls work on iOS
-   Demo code clean up
-   YouTube quality selection removed due to their poor support for it. As a result, the `qualityrequested` event has been removed
-   Controls spacing improvements
-   Fix for pressed property missing with custom controls (Fixes #1062)
-   Fix #1153: Captions language fallback (thanks @friday)
-   Fix for setting pressed property of undefined (Fixes #1102)
2018-08-14 00:02:01 +10:00
48bf368316 Merge pull request #1160 from sampotts/develop
v3.4.0
2018-08-14 00:00:24 +10:00
8f94ce86a0 Merge branch 'master' into develop
# Conflicts:
#	readme.md
2018-08-13 23:59:19 +10:00
10a9cf08f1 Changelog 2018-08-13 23:57:46 +10:00
286d0d1794 Fix for pressed property missing with custom controls (Fixes #1062) 2018-08-13 23:52:10 +10:00
95f6fa2731 Fix for setting pressed property of undefined (Fixes #1102) 2018-08-13 23:46:58 +10:00
1aeef81288 Controls spacing improvements 2018-08-13 23:43:22 +10:00
211ad6c8f5 Removed YouTube quality controls 2018-08-13 23:43:08 +10:00
468b20d227 Moved mute button inside plyr__volume 2018-08-13 23:42:12 +10:00
f6bc42c2bc Fix IE11 issue in demo 2018-08-13 23:03:08 +10:00
2c01b8ba76 Yarn lock file 2018-08-13 23:02:31 +10:00
4e1df8677f Fix tooltip alignment 2018-08-13 23:02:14 +10:00
6953a12e2a Set background color for video 2018-08-13 23:01:56 +10:00
1d0db89194 Update wrong reference in docs 2018-08-13 23:01:38 +10:00
297f297d18 Moved i18n to utils 2018-08-13 21:39:16 +10:00
059205c378 Package updates 2018-08-13 21:39:02 +10:00
f94e53ffb1 Merge pull request #1158 from friday/1153
Fix #1153: Captions language fallback
2018-08-13 09:24:25 +10:00
a4f1fdec5d Fix #1153: Captions language fallback 2018-08-12 20:12:22 +02:00
75374eb154 Merge pull request #1147 from jamesoflol/fix-ios-fullscreen-while-stopped
Remove 'video is playing' requirement for iosNative fullscreen
2018-08-05 22:46:58 +10:00
3ad118c026 3.4.0-beta.2 2018-08-05 22:43:35 +10:00
0bc6b1f1b3 Fix issue where enter key wasn’t setting focus correctly 2018-08-05 22:41:21 +10:00
4ea458e1a3 Rounded aria-valuetext to 1 decimal place 2018-08-05 21:48:42 +10:00
aacb172017 Removed aria-labelled-by 2018-08-05 21:48:21 +10:00
dbf768b1bd Remove 'video is playing' requirement for iosNative fullscreen 2018-08-03 09:58:51 +10:00
b96fcfc8ac v3.4.0-beta.1 2018-08-02 00:55:48 +10:00
18b4d26bee Merge pull request #1142 from sampotts/a11y-improvements
A11y improvements
2018-08-02 00:47:57 +10:00
7f4b74e2d4 Fix for hover over iframed players not showing controls 2018-08-02 00:47:03 +10:00
a8f8486cf4 Merge pull request #1143 from mhluska/patch-1
Fix Readme typo (Patron -> Patreon)
2018-08-01 15:09:00 +10:00
a343e58e53 Fix Readme typo (Patron -> Patreon) 2018-08-01 12:08:02 +07:00
0892d69ba2 Handle race condition for ads lib loading after source change 2018-08-01 13:56:49 +10:00
ba511b51c7 Box shadow fix for range track 2018-08-01 13:00:51 +10:00
e090581913 Ads on dev or prod only 2018-08-01 11:49:42 +10:00
aaa56caa9c Only focus button if menu wasn’t hidden already 2018-08-01 01:38:57 +10:00
c8db1e55dd Escape closes menu 2018-08-01 01:26:15 +10:00
58079393e6 Build 2018-08-01 00:58:27 +10:00
0b44f2d897 Demo config 2018-08-01 00:57:45 +10:00
2371619486 Linting 2018-08-01 00:56:44 +10:00
13a54b5dbe Merge branch 'develop' into a11y-improvements
# Conflicts:
#	src/js/controls.js
2018-08-01 00:46:26 +10:00
fa0861ff2e Merge pull request #1141 from friday/1137
Improve captions positioning consistency
2018-08-01 00:41:48 +10:00
748aa5179f Comments about keydown vs keyup for Firefox 2018-08-01 00:38:19 +10:00
56a485bac6 Fix Firefox spacebar issue 2018-08-01 00:37:55 +10:00
9488de30e5 Fix #1137: Improve captions positioning consistency 2018-07-31 16:26:34 +02:00
e3dfd16096 Merge pull request #1139 from friday/controls-input
Controls input fixes
2018-07-31 09:08:08 +10:00
c230ccce86 Update controls.md docs 2018-07-31 00:44:07 +02:00
db22a8e9c4 Improve handling of the 'controls' argument 2018-07-31 00:43:56 +02:00
3a3358e2b4 Make iOS range fix more universal 2018-07-30 23:29:14 +10:00
248005e8e0 Fix merge 2018-07-30 23:29:02 +10:00
dae272ef66 Merge branch 'develop' into a11y-improvements
# Conflicts:
#	demo/dist/demo.css
#	dist/plyr.css
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	package.json
#	src/js/plyr.js
2018-07-30 23:09:12 +10:00
2679c5898e v3.2.23 2018-07-30 22:55:34 +10:00
efb7401e6d Merge pull request #1136 from sampotts/develop
v3.3.23
2018-07-30 22:54:03 +10:00
60a0f0c979 Missed reference 2018-07-30 22:53:42 +10:00
5d168d0e14 v3.3.23 2018-07-30 22:53:09 +10:00
f964e34d8c Merge pull request #1134 from mjfwebb/hide-empty-controls
Hide empty controls
2018-07-30 22:47:10 +10:00
021ba0b8e9 Update controls.scss 2018-07-30 22:46:48 +10:00
96d371546c Merge pull request #1135 from sampotts/master
Merge back
2018-07-30 22:45:59 +10:00
e1780a4df0 Fix for redraw issue 2018-07-30 22:44:19 +10:00
e5e169a1e2 Don't move caption up when "showing" the lower controls when the controls are empty 2018-07-30 01:02:13 +02:00
5eda498516 If the plyr__controls is empty it is still showing the transition causing captions to be pushed up when hovering over where the controls would be. This change hides the plyr__controls div when it is empty. 2018-07-29 22:02:16 +02:00
599b33e55f Click to play fix, poster fix, iOS controls fixes 2018-07-30 01:13:12 +10:00
3a8332bdb3 Fix for webkit redrawing issue 2018-07-29 12:32:26 +10:00
44b5d9f6b9 Merge pull request #1131 from friday/1108
Make sure youtube.onReady doesn't run twice
2018-07-29 00:19:37 +10:00
24deff0e2d Fix #1108: Make sure youtube.onReady doesn't run twice 2018-07-28 04:04:57 +02:00
0933b48c2a Merge pull request #1113 from friday/issue-templates
Use GitHub's new issue template prompt
2018-07-27 13:12:49 +10:00
71578e07ec Merge pull request #1120 from didacte/add-missing-youtube-hl-param
Add support for YouTube's hl param
2018-07-26 00:03:10 +10:00
671325dd17 Add support for YouTube's hl param 2018-07-24 16:36:25 -04:00
53a3d06103 Merge branch 'develop' into a11y-improvements
# Conflicts:
#	demo/dist/demo.css
#	demo/dist/demo.js.map
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.css
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	package.json
#	yarn.lock
2018-07-24 09:38:26 +10:00
ad1989e45e New issue templates (with prompt) 2018-07-23 06:21:46 +02:00
f9ac98bc6d Lowercase contributing.md (like the other docs) 2018-07-23 06:21:25 +02:00
544ab0086b Logic fix 2018-07-19 10:02:41 +10:00
13bf80d372 Deployment improvements (auto purge cache etc) 2018-07-19 09:47:11 +10:00
e2fb922d73 v3.3.22 2018-07-18 21:47:46 +10:00
a6cc85c437 Merge branch 'develop' 2018-07-18 21:46:43 +10:00
d061be5d2b Changelog 2018-07-18 21:46:23 +10:00
dc2feedd79 Merge pull request #1103 from sampotts/develop
v3.3.12
2018-07-18 21:45:31 +10:00
f8e4ba36e5 Merge branch 'master' into develop
# Conflicts:
#	changelog.md
#	dist/plyr.js.map
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js.map
#	package.json
2018-07-18 21:44:39 +10:00
f3d5389587 3.3.17 2018-07-18 21:41:46 +10:00
d9ffb10b93 Merge pull request #1080 from gurupras/html5-quality-source-setter-readme
Updated README.md to show how to add quality options to HTML5 videos
2018-07-18 15:32:46 +10:00
e63ad7c74b Keyboard and focus improvements 2018-07-15 19:23:28 +10:00
1186377b25 Merge pull request #1095 from friday/stickler
Add stickler config
2018-07-11 23:32:49 +10:00
8616895e57 Merge pull request #1094 from friday/remark
Verify internal documentation links with "remark"
2018-07-11 09:17:24 +10:00
2cf5a22c85 Fix internal link for the source setter 2018-07-10 23:56:00 +02:00
763eb2df80 Add 'remark' and plugin to verify internal links in markdown 2018-07-10 23:56:00 +02:00
8bbf66a0fb Add stickler config 2018-07-10 20:53:09 +02:00
676b46e4a7 Merge pull request #1093 from friday/travis-2
Verify PR instructions with Travis
2018-07-10 15:24:22 +10:00
82a119c67f Add travis check for the base branch (only permit develop for code changes) 2018-07-10 05:16:05 +02:00
6fd4389887 Add travis check for omitting dist in development branch 2018-07-10 05:16:05 +02:00
1e1a548459 Simplify travis conf 2018-07-10 02:44:15 +02:00
8db9b53a8f Merge pull request #1092 from friday/readme-streaming-link
Fix streaming link in docs
2018-07-10 09:50:43 +10:00
ba33fd8277 Merge pull request #1091 from friday/1085
Add navigator.languages fallback for ios 9
2018-07-10 09:50:24 +10:00
8071feda18 Fix streaming link 2018-07-09 18:47:16 +02:00
a49b73cd01 Add navigator.languages fallback for ios 9 2018-07-09 18:38:23 +02:00
8226493a9e Update pull_request_template.md 2018-07-05 09:49:25 +10:00
38a8a0e8a1 Update issue_template.md 2018-07-05 09:45:17 +10:00
ead6601394 Merge 2018-07-02 23:11:59 +10:00
e61ebd8d05 Merge branch 'develop' into a11y-improvements 2018-07-02 23:11:50 +10:00
6eeca8b5d1 Merge pull request #1083 from friday/fix-travis
Change "no-cycle" lint-error to warning
2018-07-02 08:53:50 +10:00
bf51ce4414 Merge pull request #1082 from friday/yarn-error.log
Gitignore yarn-error.log
2018-07-02 08:53:32 +10:00
5ad614e251 Change linting import/no-cycle to warning (not error) 2018-07-01 19:50:58 +02:00
93c890603d Gitignore yarn-error.log 2018-07-01 19:33:47 +02:00
29fb4dfc2b Updated README.md to show up to add quality options to HTML5 videos
initialized via the source setter
2018-06-30 08:04:32 -04:00
7de9fd1d65 Merge branch 'develop'
# Conflicts:
#	changelog.md
#	demo/dist/demo.css
#	demo/dist/demo.js.map
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.css
#	dist/plyr.js.map
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	package.json
#	readme.md
#	src/js/plyr.js
#	src/js/plyr.polyfilled.js
2018-06-29 00:43:02 +10:00
566c059832 3.3.16 2018-06-29 00:38:11 +10:00
0c9572f0a1 Merge branch 'develop' of github.com:sampotts/plyr into develop 2018-06-29 00:21:28 +10:00
c99607c85a Linting, housekeeping, duration fix (fixes #1074) 2018-06-29 00:21:22 +10:00
e2010bcd1a Merge pull request #1075 from mimse/feature/handle_live_stream
Hide currentTime and progress
2018-06-28 23:52:58 +10:00
3bf1c59bd6 Work on key bindings for menu 2018-06-28 23:44:07 +10:00
e9f1b55f51 Hide currentTime and progress 2018-06-28 11:25:55 +02:00
4f5152f526 Merge pull request #1070 from mimse/fix_condition_check
Fixed condition check
2018-06-27 22:47:42 +10:00
de9b53045a Fixed condition check
If class includes "control" it will add it again.
2018-06-27 14:33:51 +02:00
e59fe1aacf Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	src/js/listeners.js
2018-06-25 23:09:13 +10:00
df458c5e7a Merge branch 'develop' of github.com:sampotts/plyr into develop 2018-06-25 22:31:43 +10:00
e206554146 Linting and package tweak 2018-06-25 22:31:38 +10:00
e422806c44 Merge pull request #1063 from klassicd/develop
Handle undefined this.player.elements.buttons.play
2018-06-25 20:21:49 +10:00
b6ddf144f4 handle undefined player.elements.buttons.play 2018-06-25 12:00:02 +02:00
86406ee59a Merge pull request #1061 from friday/captions-no-toggle-button
Fix captions.toggle() if there is no toggle button
2018-06-22 08:19:33 +10:00
81c5477f1d Fix captions.toggle() if there is no toggle button 2018-06-21 15:22:30 +02:00
ac64350a5f Fix for bug where controls wouldn't show on hover over YouTube video 2018-06-21 19:26:56 +10:00
333619f1e3 Remove pointer-events: none on embed <iframe> to comply with YouTube ToS 2018-06-21 14:26:40 +10:00
17dcb63c26 Merge branch 'develop'
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
2018-06-21 09:11:05 +10:00
e04b90c9c0 Ads only on HTML5 and .is cleanup 2018-06-21 09:06:28 +10:00
1f1d74ba50 Work on menus 2018-06-21 09:01:16 +10:00
f62e1da01a Merge pull request #1057 from meyt/patch-1
Fix i18n defaults path on README
2018-06-20 22:46:50 +10:00
2fe949629f Fix i18n defaults path 2018-06-20 16:00:51 +04:30
20f2ddc11d Merge pull request #1056 from friday/volume
Minor increaseVolume and decreaseVolume changes
2018-06-20 16:53:43 +10:00
004528a65c Avoid conditions in volume scroll event listener 2018-06-19 17:22:12 +02:00
39c7bd40c2 Make decreaseVolume wrap increaseVolume for code reuse 2018-06-19 16:29:52 +02:00
43879e08f4 Make (increase/decrease)Volume methods ignore invalid input instead of raising / lowering to the min / max 2018-06-19 16:27:06 +02:00
bb546fe43f Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
2018-06-19 19:24:47 +10:00
5ed7aa6620 v3.3.17 2018-06-19 16:58:38 +10:00
47750b6aad v3.3.17
-   Fix YouTube muting after seeking with the progress slider
-   Respect preload="none" when setting quality if the media hasn't been loaded some other way
2018-06-19 16:57:32 +10:00
de7832eb8b Merge branch 'develop' 2018-06-19 16:40:54 +10:00
52ea5bd0ab Merge pull request #1052 from friday/youtube-audio-fix
Fix YouTube muting after seeking with the progress slider.
2018-06-19 04:15:07 +02:00
457d112df7 Fix #1045: YouTube mutes when seeking after play 2018-06-19 04:03:59 +02:00
22cdec9d38 Merge pull request #1051 from friday/quality-2
Respect preload="none" when setting quality if the media hasn't been loaded some other way
2018-06-19 11:48:44 +10:00
d72e502107 Fixes #1044: Don't load the new source if preload is disabled and the current source hasn't been loaded 2018-06-19 03:39:18 +02:00
94055f0772 Replace filter()[0] with find() 2018-06-19 03:35:57 +02:00
ede9323524 v3.3.16 2018-06-19 09:16:14 +10:00
c45f428f61 Built files 2018-06-19 09:14:10 +10:00
b61ba02f3d Fix issue with play button not changing state (fixes #1048) 2018-06-19 09:12:21 +10:00
9e1218547b WIP 2018-06-19 09:11:35 +10:00
715b88c09b Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
2018-06-18 23:29:25 +10:00
ea4d91d2a0 v3.3.15 2018-06-18 23:21:03 +10:00
7b9ef7d757 More work on menus 2018-06-18 23:13:40 +10:00
d64ed4ba5a Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
2018-06-18 22:17:34 +10:00
22d524ac9d Removed 1440p so I can afford to eat 2018-06-18 22:16:12 +10:00
8584f6a1db v3.3.14 2018-06-18 22:01:56 +10:00
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
cc3c0b5448 Merge branch 'develop' 2018-06-18 21:41:25 +10:00
ffd864ed39 Work on controls 2018-06-18 21:39:47 +10:00
3c9c1b4cdc Merge pull request #1041 from sampotts/a11y-improvements
A11y improvements
2018-06-17 01:34:11 +10:00
599883e684 Formatting fix 2018-06-17 01:30:24 +10:00
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
d4abb4b143 120 line width, package upgrade 2018-06-17 01:04:55 +10:00
828ce66942 Merge pull request #1038 from friday/captions-input-lowercase
Small captions fixes
2018-06-17 00:40:56 +10:00
ccc2608cf6 Merge pull request #1039 from friday/poster-race-conditions
Fix poster race conditions
2018-06-17 00:40:28 +10:00
de45de0e0b Merge pull request #1040 from friday/switches-get-stitches
Switches code optimizations
2018-06-17 00:39:35 +10:00
99c10aa1fc Replace switch in controls.createProgress with object literal 2018-06-16 07:27:04 +02:00
2a186e425b Replace switch in support.mime with object literal and conditions, and make it return boolean 2018-06-16 07:27:04 +02:00
64bb206d85 Replace switch in support.check with simpler conditions 2018-06-16 07:27:04 +02:00
2d6732d580 Replace switch in controls.createLabel with object literal 2018-06-16 07:25:34 +02:00
8f359adf9c Fix captions.toggle order 2018-06-16 01:34:55 +02:00
1f09493ba2 Captions: Handle uppercase input (like before) 2018-06-16 01:07:16 +02:00
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
2af60c5c0d Add 'ready' promise 2018-06-15 23:01:33 +02:00
aab2817ddc Copy poster when creating new media element for YouTube and Vimeo (needed for #1018) 2018-06-15 22:57:16 +02:00
f1c4752036 Filter out null / undefined in elements.setAttributes 2018-06-15 22:52:19 +02:00
88735e3146 Replace switch in controls.updateSetting with condition 2018-06-15 15:57:10 +02:00
c373ed72d7 Replace switch in YouTube error handler with object literal 2018-06-15 15:57:10 +02:00
213cfe8c84 Replace switch in media.js with simpler conditions 2018-06-15 15:57:10 +02:00
87ea5e14b4 Replace provider switch plyr.js with conditions 2018-06-15 15:57:10 +02:00
2aa967aba9 Replace switch in source.js with destructuring 2018-06-15 12:33:30 +02:00
d522e40594 Merge pull request #1034 from friday/remove-array-newline-rule
Suggestion: Remove array newline rule
2018-06-15 15:34:29 +10:00
3cd2b9a6c3 Merge pull request #1036 from friday/captions-passive-toggle
Captions fixes (again)
2018-06-15 15:33:39 +10:00
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
cf5f77c709 Fix menu transitionend event listener 2018-06-15 05:51:23 +02:00
4811e3333f Merge pull request #1035 from sampotts/friday-contrib-2
Contributions improvements
2018-06-15 11:04:33 +10:00
8257857075 Wrap caption toggle event listener callback to avoid sending event 2018-06-15 02:44:00 +02:00
e3e4e60fdb Contributions improvements
General improvements and new sections
2018-06-14 19:50:59 +02:00
6ce9a94932 Move internal event listeners for captions with direct handling in the captions object 2018-06-14 16:41:16 +02:00
fa5d0ad316 Move toggleCaption internals to captions object 2018-06-14 15:58:35 +02:00
6bff6b317d Remove line breaks in arrays 2018-06-13 23:27:35 +02:00
99ac8d4c52 Remove array-newline rule 2018-06-13 22:07:32 +02:00
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
2fe98f3721 Fix condition in events.toggleListener to allow non-elements 2018-06-13 14:29:55 +02:00
5c08363400 Merge pull request #1030 from friday/event-improvements
Minor event changes
2018-06-13 10:52:17 +10:00
927326f715 Also remove 'once' event listeners when destroying (they may still be waiting) 2018-06-12 20:00:41 +02:00
53933dff7e Use toggleListener in trapFocus 2018-06-12 19:39:26 +02:00
f15c1344b0 Removed support for multiple elements in toggleListener 2018-06-12 19:10:00 +02:00
fb48b330cc typo 2018-06-12 17:41:17 +02:00
5dddf8b0ec Logic cleanup 2018-06-13 00:56:31 +10:00
0ecf7e3854 Force string on format 2018-06-13 00:48:42 +10:00
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
7158e507ad Merge pull request #1029 from cky917/develop
remove event listeners in destroy()
2018-06-13 00:05:31 +10:00
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
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
ee4c044d27 fix: html5.cancelRequest not remove source tag correctly 2018-06-12 11:35:31 +08:00
0b09b8ee6f Merge pull request #1027 from friday/quality
Minor code improvements for quality switching
2018-06-12 11:13:34 +10:00
db95b3234f Move uniqueness filter from getQualityOptions to setQualityMenu 2018-06-12 02:31:18 +02:00
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
81ee3f759c Remove todo comment about Vimeo support for setting quality (they don't support it) 2018-06-12 02:31:18 +02:00
ed606c28ab Filter out unsupported mimetypes in getSources() instead of the quality setter 2018-06-12 02:31:18 +02:00
f15e07f7f5 Simplify logic in youtube.mapQualityUnit (not that it matters much now) 2018-06-12 02:31:04 +02:00
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
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
840e31a693 v3.3.12 2018-06-11 17:10:37 +10:00
1bc452c349 Merge 2018-06-11 16:54:35 +10:00
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
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
abd1182303 Merge 2018-06-11 16:45:40 +10:00
efe70ab48e v3.3.11 2018-06-11 16:39:35 +10:00
62c263bda3 Replace quality setter conditions with Array.find() 2018-06-11 08:23:08 +02:00
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
38f10d4cc6 WIP 2018-06-11 16:19:11 +10:00
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
cc97d7be6a Fix synthetic event bubble/proxy loses detail 2018-06-11 08:00:46 +02:00
f951cb372c Merge pull request #1023 from friday/make-utils-static
Make utils static
2018-06-11 14:41:06 +10:00
37a3ab202a Remove wrapper function around utils.is.element in Plyr.setup() (no lnger needed) 2018-06-11 05:44:57 +02:00
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
16828e975a Move utils.is.getConstructor() to utils.getConstructor() 2018-06-11 05:44:57 +02:00
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
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
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
94699f3255 Fix problem with YouTube and Vimeo seeking while playing 2018-06-11 02:21:00 +02:00
d3e98eb27e Vimeo: Assure state is updated with autoplay (fixes #1016) 2018-06-11 00:00:16 +02:00
41012a9843 Typo 2018-06-10 22:00:15 +02:00
c83487a293 Fix #1017, fix #980, fix #1014: Captions rewrite (use index internally) 2018-06-10 19:00:07 +02:00
1fab4919c0 controls.createMenuItem: Change input to object (too many params made it hard to read) 2018-06-10 18:57:19 +02:00
9dc0f28800 Avoid condition in getTracks 2018-06-10 18:57:19 +02:00
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
a80b31bf98 Fix #1003: Formatted captions issue 2018-06-10 18:56:13 +02:00
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
76bb299c68 Restore default 2018-06-09 12:05:37 +10:00
0c03accd41 Fix Sprite issue 2018-06-09 12:04:53 +10:00
b12eeb0eb7 Merge captions setText and setCue into updateCues (fixes #998 and vimeo cuechange event) 2018-06-08 11:44:15 +02:00
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
1e1874d86b Merge pull request #1009 from friday/contributing
Contributing document and codepen demo updates
2018-06-08 10:52:37 +10:00
16624b90d3 Clarifications due to recent non-constructive comments in #1001 2018-06-07 14:30:05 +02:00
ed14b656a8 Add contributing document 2018-06-07 11:47:51 +02:00
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
05b85da3f4 Update controls.md (fixes #996) 2018-06-02 19:48:48 +10:00
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
969a877a34 v3.3.10 2018-05-31 23:41:48 +10:00
fb22a90d33 Merge pull request #993 from sampotts/develop
v3.3.10
2018-05-31 23:39:51 +10:00
108bd3dfa0 Fixed incorrect BEM formatting, fixed buffer alignment 2018-05-31 23:33:59 +10:00
5a445ae647 Merge pull request #988 from kim-company/translate-qualities
Translate quality badges and quality names
2018-05-31 22:43:31 +10:00
56668f58b6 Rename qualityName to label 2018-05-31 14:40:56 +02:00
eec96e5879 Merge pull request #990 from friday/travis
Travis integration
2018-05-31 18:33:00 +10:00
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
61f4b998e1 Wait for the metadata to be loaded before setting the currentTime 2018-05-31 10:17:41 +02:00
25a319d884 Merge pull request #989 from friday/pr-template
Proposed changes to PR template
2018-05-31 08:34:02 +10:00
4bf678fe6c Proposed changes to PR template (develop as base branch, exclude build and typo fix) 2018-05-30 19:52:15 +02:00
359acd6bb9 Lint and build in travis 2018-05-30 19:40:17 +02:00
2fce385691 Add npm scripts for linting and building 2018-05-30 19:40:17 +02:00
a82c61c539 Gulp: Add option to build only 2018-05-30 19:39:57 +02:00
a8fa125a96 Refactor getDeep method 2018-05-30 17:30:10 +02:00
6435ced707 Return undefined when the key is not present. 2018-05-30 17:10:38 +02:00
94dc0d176c Rebuild 2018-05-30 17:03:41 +02:00
23c21252e8 Rebuild 2018-05-30 17:02:07 +02:00
41c7dff0e8 Add getDeep method to utils 2018-05-30 17:02:07 +02:00
e3bae562fc Rebuild 2018-05-30 17:02:07 +02:00
1c1668bfc3 Implement translation support for qualityName and qualityBadge 2018-05-30 17:02:07 +02:00
e0c09c51f2 Allow nested translations 2018-05-30 17:02:07 +02:00
8de06fb862 Accept quality 0 2018-05-30 17:02:07 +02:00
64412868d8 ESLint tweak 2018-05-30 17:02:07 +02:00
450958c290 Merge pull request #981 from friday/hls-captions
Improve captions handling for streaming
2018-05-30 21:44:42 +10:00
963fe11ad6 Update readme.md 2018-05-30 00:22:01 +10:00
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
9d798893b5 Call duration update method manually if user config has duration 2018-05-29 16:06:07 +02:00
64399e0717 Defer initial captions update to next tick, to avoid event being triggered to early 2018-05-28 17:54:25 +02:00
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
812e07b734 Replace browser language detection in defaults.js with explicit 'auto' option 2018-05-28 07:43:37 +02:00
c9298fde76 Fix typo 2018-05-28 07:08:38 +02:00
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
813f703211 Add option to watch caption track changes and update language options 2018-05-28 06:08:03 +02:00
7aad747c25 Optimize captions code reused and ensure captionsenabled/captionsdisabled
will be sent on initial setup
2018-05-28 05:44:54 +02:00
d70a787af1 Merge branch 'develop' 2018-05-28 10:42:11 +10:00
69bb0917ad v3.3.9 2018-05-28 10:41:51 +10:00
90c5735904 WIP 2018-05-28 10:19:07 +10:00
6f256d09b2 ESLint tweak 2018-05-28 10:18:04 +10:00
e9684c2021 Merge pull request #979 from friday/respect-storage
Respect storage being disabled for storage getter
2018-05-28 10:09:02 +10:00
bf91a0e73f Merge pull request #977 from friday/utils.is.icue-ie11
Restore utils.is.cue()
2018-05-28 10:08:42 +10:00
14b6309aef Merge pull request #978 from friday/ie-issues
Fix InvalidStateError and IE11 issues
2018-05-28 10:08:24 +10:00
6391ced99f If storage is disabled, disable get as well, not just set 2018-05-28 01:58:06 +02:00
fac8a185ba Simplify currentTime setter and bail when media hasn't loaded 2018-05-28 00:57:07 +02:00
c69aa8a42b Avoid duration getter returning NaN before element has loaded 2018-05-28 00:57:01 +02:00
f34bf22125 Restore utils.is.cue() 2018-05-27 20:28:48 +02:00
f0be913dc3 Merge pull request #975 from sampotts/develop
v3.3.8
2018-05-26 13:55:22 +10:00
cd51788b98 v3.3.8 2018-05-26 13:53:15 +10:00
edd67b0da3 Typo 2018-05-20 23:44:40 +10:00
d733454d7f Pause while seeking 2018-05-20 23:40:28 +10:00
41f9a87e0e Add URL polyfill 2018-05-20 23:40:00 +10:00
f4858f0c62 Merge pull request #959 from friday/876
Youtube and vimeo fixes
2018-05-19 16:49:31 +10:00
121093ae71 Prevent durationchange events from showing time when invertTime is false 2018-05-19 04:27:45 +02:00
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
723298a07b Fix #921: Trigger seeked event in youtube plugin if either playing or paused 2018-05-19 04:18:27 +02:00
f8c89e3e95 Fix #876: YouTube and Vimeo autoplays on seek 2018-05-19 04:18:27 +02:00
333435a9c2 Fix playback state (paused) and events (play/pause) 2018-05-19 04:18:27 +02:00
3ab2295fe7 Merge branch 'master' into develop 2018-05-19 11:29:47 +10:00
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
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
3bba65f2c2 Merge pull request #967 from friday/883
toggleControls rewrite
2018-05-19 11:27:19 +10:00
1bab0d07b5 Merge branch 'master' into develop 2018-05-19 11:26:05 +10:00
602353f4d9 Merge branch 'master' of github.com:sampotts/plyr 2018-05-19 11:25:02 +10:00
51814249af Reduce circular dependencies 2018-05-19 11:24:56 +10:00
37c5fbfe16 toggleControls() rewrite 2018-05-18 16:19:38 +02:00
d7356726a1 Remove ui.checkFailed() and error class 2018-05-16 23:21:06 +02:00
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
28826f6402 Add 'video only' caveat to toggleControls() doc (current behavior) 2018-05-16 23:21:06 +02:00
c845558d96 Youtube poster: Set css backgroundSize to 'cover' for padded youtube thumbnails 2018-05-15 16:41:51 +02:00
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
90d5b48845 Add async method to utils for loading/checking images 2018-05-15 13:27:55 +02:00
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
797b70998f Merge pull request #960 from friday/935
Support importing Plyr in Node.js without errors
2018-05-14 23:38:48 +10:00
4a01027da0 Merge pull request #961 from friday/expose-defaults
Enable overriding defaults
2018-05-14 23:38:25 +10:00
7ca2169790 Expose defaults (enable overriding) 2018-05-14 06:49:04 +02:00
054f522aa9 Enable importing Plyr in node.js without errors (resulting in an empty object) 2018-05-14 05:35:13 +02:00
f2fc3f5ea5 Fix the seek tooltip time difference from seek time 2018-05-12 00:10:39 +02:00
765c01e83d Remove references to window.Plyr 2018-05-10 09:34:15 +10:00
33a11fb53a v3.3.7 2018-05-09 09:50:22 +10:00
d1d41ca49a Merge branch 'master' of github.com:sampotts/plyr 2018-05-09 09:48:52 +10:00
c06e0ee5e9 Grid tweak 2018-05-09 09:48:46 +10:00
83f80ccc40 Merge pull request #950 from friday/poster-fixes
Poster fixes
2018-05-09 09:44:36 +10:00
069065ea3a Fix #946 - poster getting click events 2018-05-08 16:50:40 +02:00
1672e78041 Fix poster being stretched 2018-05-08 16:49:32 +02:00
34401de3d0 Merge branch 'master' into develop 2018-05-08 22:22:43 +10:00
f687b81b70 v3.3.6 2018-05-08 13:18:30 +10:00
bbb11e611e Vimeo options, docs for multiple players 2018-05-08 13:12:39 +10:00
90919411e9 Use div for poster, Vimeo fixes, Tooltip fixes 2018-05-08 12:57:24 +10:00
1491b017a0 Setup multiple players 2018-05-06 16:18:10 +10:00
1655150092 v3.3.5 2018-05-06 01:32:51 +10:00
ceb6c9a100 v3.3.3 2018-05-06 01:16:41 +10:00
00bbce08fb Reverted menu change 2018-05-06 01:14:41 +10:00
91a4b86860 Small bug fixes 2018-05-06 01:03:38 +10:00
5aece6fa06 Merge 2018-05-06 00:50:02 +10:00
a70b94afe2 Merge branch 'master' of github.com:sampotts/plyr 2018-05-06 00:49:22 +10:00
9ebc2719d3 v3.3.0 2018-05-06 00:49:12 +10:00
b46aae1833 Merge pull request #939 from Billybobbonnet/patch-1
Added 480p to SD labels
2018-05-03 20:28:03 +10:00
30e6a40865 Added 480p to SD labels 2018-05-03 11:42:09 +02:00
403df36af6 Merge branch 'master' into develop 2018-04-27 21:42:29 +10:00
5ca769807e Merge pull request #923 from friday/922
Only add hideControls class if config.hideControls is truthy
2018-04-27 20:07:18 +10:00
72a71a605b Fix for default timestamp 2018-04-27 20:06:14 +10:00
24d833a5d1 Merge branch 'master' into develop 2018-04-27 18:35:06 +10:00
44b30380f7 Merge branch 'beta' of github.com:Selz/plyr 2018-04-27 18:34:06 +10:00
261cd086c7 Update readme.md 2018-04-27 12:44:58 +10:00
9e19b526b9 Only add hideControls class if config.hideControls is truthy 2018-04-26 17:51:14 +02:00
6c617a0ef1 Readme fix 2018-04-27 01:12:04 +10:00
a812650fea v3.2.4 2018-04-27 00:47:51 +10:00
fec7a77d6f v3.2.3 2018-04-25 20:02:36 +10:00
971e261067 Fix for iOS 9 throwing error for name property in fullscreen API (fixes #908) 2018-04-25 19:59:22 +10:00
27407ba021 v3.2.2 2018-04-25 19:46:39 +10:00
ef8e58ede4 Fix for hidden buffer and incorrect use of aria-hidden 2018-04-25 19:40:23 +10:00
f13260c10a Merge pull request #919 from sampotts/master
Merge back
2018-04-25 07:38:17 +10:00
e1183d6049 Merge pull request #918 from sampotts/master
Merge back
2018-04-25 07:37:18 +10:00
f1b275aedc v3.2.1 2018-04-23 00:53:54 +10:00
b647af256c More a11y stuff and context menu fix 2018-04-23 00:01:19 +10:00
d2e9ed3467 Merge 2018-04-18 18:34:59 +10:00
5b39986835 Merge branch 'master' of github.com:sampotts/plyr 2018-04-18 18:29:50 +10:00
a97b08e8ea ARIA and Vimeo fixes 2018-04-18 18:29:43 +10:00
56d1be9447 Merge pull request #903 from friday/901
Show captions even if toggle button is omitted from controls
2018-04-18 08:49:05 +10:00
a241cb5215 Merge pull request #904 from friday/881
Fullscreen aria-pressed event listened fix for Chrome
2018-04-18 08:48:08 +10:00
042b1a8294 Fullscreen aria-pressed event listened fix for Chrome 2018-04-17 20:28:47 +02:00
6d79b8cd4c Don't require captions toggle button to be enabled in order to show captions 2018-04-17 18:59:19 +02:00
88d766aeae v3.2.0 2018-04-17 23:54:38 +10:00
119b471b84 More bug fixes 2018-04-17 23:51:23 +10:00
7f079e0ec3 Fix for playing false positive (fixes #898) 2018-04-17 22:52:46 +10:00
46fe3eecff Fixed bug for captions with no srclang and labels and improved logic (fixes #875) 2018-04-17 22:49:28 +10:00
3061a701d5 PR merge 2018-04-14 14:58:09 +10:00
e45109e1d7 Merge branch 'master' of github.com:sampotts/plyr 2018-04-14 14:48:20 +10:00
e138e6d51e Merge pull request #895 from nicolasthy/patch-1
Fix IE10 split error
2018-04-14 14:47:57 +10:00
aef1363b04 Fix IE10 with default captions.language 2018-04-13 14:44:05 +02:00
766dd03d81 Fix IE10 split error
On IE10, Plyr throws the error `Unable to get property 'split' of undefined or null reference`. This fixes the case when `window.navigator.language` is null and can't use the `split()` function.
2018-04-12 22:12:12 +02:00
ab393651ec Merge branch 'master' of github.com:sampotts/plyr 2018-04-11 23:44:44 +10:00
ffd265d0ae Merge pull request #888 from Antonio-Laguna/master
Safer check for active caption
2018-04-11 23:42:40 +10:00
72155472dd Safer check for active caption 2018-04-11 15:39:12 +02:00
9b7170834e Merge pull request #887 from danielsarin/use-i18n-for-normal-speed
Add i18n support for "Normal" value in speed options
2018-04-11 22:43:47 +10:00
3e57a87bf7 Add i18n support for "Normal" value in speed options 2018-04-11 15:39:23 +03:00
a15d1c9f1c Merge pull request #886 from danielsarin/increate-menu-z-index
Increase menu container z-index to be higher than controls
2018-04-11 22:23:46 +10:00
a095a64f90 Increase menu container z-index to be higher than controls 2018-04-11 15:13:34 +03:00
2374d6b1c4 Merge branch 'master' of github.com:sampotts/plyr 2018-04-11 21:52:36 +10:00
5ed3ff9084 Restore paused state after seek 2018-04-11 21:52:31 +10:00
385be55510 Merge pull request #874 from friday/873
Fixes issue leaving fullscreen in Chrome using button
2018-04-10 17:21:15 +10:00
3082d0d128 Fixes #873 Can't leave fullscreen in Chrome (using button) 2018-04-05 20:29:01 +02:00
f7e242f054 Merge pull request #871 from friday/867
Fix #867: Add custom property fallback
2018-04-05 09:19:46 +10:00
2874505004 Merge pull request #868 from friday/null-no-controls
Fix string "null" being appended after the video if controls argument is empty.
2018-04-05 09:19:05 +10:00
ed9e0c13d7 Fix #867: Add custom property fallback 2018-04-05 00:33:09 +02:00
10be94fa99 Fix 'null' being appended after the video if controls is empty array 2018-04-04 21:33:14 +02:00
ee79c46145 Merge branch 'master' of github.com:sampotts/plyr 2018-04-04 16:50:06 +10:00
384010a2c0 Style fixes 2018-04-04 16:50:00 +10:00
1e47019122 Merge pull request #863 from friday/data-plyr-config-no-options
Fix loading data-plyr-config when initiating Plyr without any options
2018-04-04 11:33:30 +10:00
536c65e82c Fix loading data-plyr-config when initiating Plyr without any options 2018-04-04 03:16:25 +02:00
cdf14932ec Changelog 2018-04-03 23:03:16 +10:00
3b20dbd9fd v3.1.0 2018-04-03 22:57:34 +10:00
e4d975af00 Styling fixes 2018-04-03 22:56:19 +10:00
2782a00e7c v3.1.0-beta.2 2018-04-03 22:31:55 +10:00
91d192dd7c YouTube speed menu fix 2018-04-03 22:30:29 +10:00
b1e3abc795 v3.1.0-beta.1 2018-04-02 22:52:02 +10:00
3395e8df90 HTML5 quality selection 2018-04-02 22:40:03 +10:00
cce143a7da v3.0.11 2018-03-30 23:14:07 +11:00
d593005b32 Muted and autoplay fixes, small bug fixes 2018-03-30 23:09:17 +11:00
7be9d5d4d3 v3.0.10 2018-03-30 00:16:42 +11:00
d7141d5ed7 Controls docs, package upgrades 2018-03-30 00:11:48 +11:00
0d0ece94d3 Fix regression 2018-03-29 20:20:37 +11:00
1c06f6d06d Vimeo hotfix 2018-03-29 19:35:02 +11:00
dda8e30b92 Merge branch 'master' of github.com:sampotts/plyr 2018-03-28 22:45:18 +11:00
c4e2e24643 Bug fixes 2018-03-28 22:45:11 +11:00
e020a105a3 Update readme.md 2018-03-28 00:55:55 +11:00
2b7fe9a4f9 v3.0.6 2018-03-28 00:17:15 +11:00
951df64b7f v3.0.5 2018-03-27 23:52:26 +11:00
0976afe282 v3.0.4 2018-03-27 23:47:58 +11:00
7b1e4abda7 Controls fixes 2018-03-27 23:43:38 +11:00
0cf75eed3f Revert API method change 2018-03-27 21:15:11 +11:00
d96957d086 Allow fullscreen in iframe 2018-03-27 21:13:22 +11:00
1a032ea498 Fix for seeking issue 2018-03-27 21:10:06 +11:00
5d079da1b8 Use object.entries 2018-03-27 10:41:06 +11:00
9c1bc6ab08 Fixes for fast forward and issues with event.preventDefault() 2018-03-27 10:36:08 +11:00
3d2ba8c009 Update readme.md 2018-03-22 09:11:42 +11:00
e872ce3f77 Update readme.md 2018-03-22 09:10:50 +11:00
b77756da04 Typo 2018-03-22 01:15:10 +11:00
9b23e13ce8 v3.0.3 2018-03-22 01:13:37 +11:00
5eafe9baff Vimeo offset tweak (fixes #826) 2018-03-22 01:08:08 +11:00
c251c94131 Fix for .stop() method (fixes #819) 2018-03-22 01:02:38 +11:00
17041efc71 Check for array for speed options (fixes #252) 2018-03-22 00:33:14 +11:00
05b8e8a6e0 Restore as float (fixes #828) 2018-03-22 00:28:42 +11:00
f998b996fa Fix for Firefox fullscreen oddness (Fixes #821) 2018-03-22 00:26:01 +11:00
958b47c435 Merge branch 'master' of github.com:sampotts/plyr 2018-03-22 00:06:26 +11:00
a27248d3b6 Merge pull request #820 from saadshahd/patch-1
Fix fast-forward control
2018-03-22 00:05:24 +11:00
1b1f7be7ff Merge branch 'master' of github.com:sampotts/plyr 2018-03-22 00:04:34 +11:00
59d4a27240 Improve Sprite checking (fixes #827) 2018-03-22 00:04:28 +11:00
75e9f3c2e3 Fix fast-forward control
fast-forward control doesn't work.
2018-03-21 12:15:57 +02:00
7132eccf50 Merge pull request #822 from DanielRuf/patch/fix-options-link
fix the options link in the readme
2018-03-21 09:12:06 +11:00
e953c6398c fix the options link in the readme 2018-03-20 15:05:51 +01:00
bb7eea27e5 v3.0.2 2018-03-18 22:46:36 +11:00
595c5e95bc Fix for Safari with adblockers 2018-03-18 22:37:06 +11:00
43e6dcd41d Fix for local storage issue 2018-03-18 01:37:24 +11:00
b06c8ae43f Changelog updated 2018-03-18 01:14:18 +11:00
c7ea13c0c7 Sentry in live only 2018-03-18 01:08:05 +11:00
0f8c6e147b Added Sentry 2018-03-18 00:21:23 +11:00
e566365288 Typo 2018-03-17 23:44:40 +11:00
a06e0f5890 Updated screenshot 2018-03-17 23:40:28 +11:00
3bccc0da01 v3.0.0 2018-03-17 23:33:25 +11:00
a0173d991e Removed beta message 2018-03-17 23:31:34 +11:00
600f0eb8a3 Merge branch 'beta'
# Conflicts:
#	readme.md
2018-03-17 23:30:16 +11:00
5db73b1327 Added buffered getter 2018-03-17 23:27:40 +11:00
5cb1628cd8 Vimeo fix 2018-03-15 10:29:05 +11:00
c74b75e8e1 3.0.0-beta.20 2018-03-13 23:35:17 +11:00
0538476d6f 3.0.0-beta.19 2018-03-13 22:15:28 +11:00
5ebe18d081 Typography fix 2018-03-13 22:00:40 +11:00
207adde36d 3.0.0-beta.18 2018-03-13 21:44:18 +11:00
1b13ddaa54 Update ads 2018-03-13 21:42:01 +11:00
9981c349be Fix for null manager race condition 2018-03-11 18:23:47 +11:00
b3365a7373 Normalised event names and removed unused 2018-03-11 12:54:51 +11:00
9a0c1c830d Merge pull request #804 from friday/ads-trigger-arguments
[v3] Add optional arguments to Ads.trigger
2018-03-11 10:55:29 +11:00
ef27ba16f4 Add optional argument to Ads.trigger (currently only used for adblocker error) 2018-03-10 16:20:33 +01:00
e206edc1f6 Event listener fixes, loadScript promise, ads tweaks 2018-03-11 02:03:35 +11:00
c734bc4957 Merge branch 'beta' of github.com:sampotts/plyr into beta 2018-03-10 23:32:55 +11:00
572b8a7aca Manually merged PRs 2018-03-10 23:32:48 +11:00
eebae4a227 Merge pull request #802 from gehaktmolen/ad-hotfixes
Advertisement couldnt be loaded when creative dimensions do not align after resizing
2018-03-10 23:32:15 +11:00
e0562752ea Merge pull request #795 from frogg/patch-1
Added link that explains Webkit's autoplay blocker
2018-03-10 23:29:22 +11:00
6a2ca534d2 Removed redundant wrappers within the adsmanager promises. 2018-03-09 14:29:37 +01:00
7adc2bc6c8 Unneeded else has been removed within the play() method. 2018-03-09 13:21:19 +01:00
ba8d7831a7 Made sure play() returns a promise. 2018-03-09 12:50:57 +01:00
69ffcbad27 Ad block detection would not work when calling play() right after creating the player instance, so the adsManager now also rejects on such a case. Also made sure that calling play() will wait for the adsManager promise to resolve or otherwise return the media.play() method. 2018-03-09 11:17:24 +01:00
819f7d1080 Resizing the ad container while having it on display none will return offset width and height of 0, which will cause ads not to play when ad sizes are set within the clients DSP. Also making sure that the inner containers of the ad container are full size. The container is now hidden/ displayed using z-index. 2018-03-07 15:43:48 +01:00
409b588458 Made sure that cue points for midrolls are not displayed when the ad rule for a midroll doesn't exceed the total play time of a video. 2018-03-07 15:17:30 +01:00
e90a603d57 Removed a double this.enabled variable and updated a comment in ads.js. Also made sure the adsmanager promise also can fail, so we can use it to wait for getting the advertisement ready when someone clicks the play button. Otherwise there it can look glitchy when the actual video starts playing and the video ad plays a few seconds later because the vast tag was slow to retrieve. Also fixed a typo. 2018-03-06 17:27:59 +01:00
6f061621ad v3.0.0-beta.17 2018-03-03 23:16:15 +11:00
0300610108 Typo 2018-03-03 23:14:57 +11:00
2fba5f152c 3.0.0-beta.16 2018-03-03 23:06:54 +11:00
317b08c703 Ready event fix, YouTube play event fix, docs update 2018-03-03 23:06:12 +11:00
e6db374a72 Added link that explains Webkit's autoplay blocker 2018-02-24 16:19:55 +01:00
bfb550b8d0 Package updates 2018-02-22 23:46:52 +11:00
174234c166 v3.0.0-beta.15 2018-02-19 09:55:16 +11:00
24b4220de5 Fix IE CORS captions 2018-02-19 09:52:46 +11:00
f1895a4cce Pause button fix, polyfilled build, unminified builds 2018-02-17 19:34:15 +11:00
c2a6306d46 Merge pull request #781 from friday/gulp-unminified-js-output
Build both minified and non-minified js-bundles
2018-02-17 09:22:34 +11:00
7ac732f45b Merge branch 'beta' into gulp-unminified-js-output 2018-02-17 09:22:19 +11:00
c90f1bdf08 v3.0.0-beta.13 2018-02-13 00:02:13 +11:00
6a9be8d16b Fix for custom controls 2018-02-13 00:01:19 +11:00
58c2c52c95 Merge branch 'beta' of github.com:sampotts/plyr into beta 2018-02-11 15:09:40 +11:00
73a39769d4 Fullscreen API changes, color settings tweaks 2018-02-11 15:09:34 +11:00
7221e26eca Merge pull request #780 from friday/captions-ie11-indexsizeerror
Fix harmless but annoying IE error 'IndexSizeError'
2018-02-06 12:51:45 +11:00
98adb8e784 Fix harmless but annoying IE error 'IndexSizeError' 2018-02-06 02:39:01 +01:00
a59dcb2f53 Gulp js build: create both minified and non-minified outputs 2018-02-06 01:57:27 +01:00
d21b58e1c9 Copy 2018-02-06 11:26:13 +11:00
d6e84cbabb Nicer checks 2018-02-06 11:12:03 +11:00
fcccf1d479 Copy 2018-02-06 11:08:26 +11:00
211db12a3d Readme merge 2018-02-06 11:06:46 +11:00
ab7f277a1b Merge pull request #769 from redxtech/add-vue-plyr-to-readme
Add vue-plyr to readme
2018-02-06 10:51:42 +11:00
ce1d5a60d6 Remove eslint-rule 'no-shadow' (common variable names should be able to exist in different scopes) 2018-02-05 23:22:20 +01:00
f67315e20c 3.0.0-beta.12 2018-02-06 00:25:50 +11:00
2150c44036 Added backwards compatibility for <div> embeds 2018-02-06 00:24:48 +11:00
70c9fbdde3 Removed fetch dependency 2018-02-05 21:43:32 +11:00
f3ea31c515 Merge branch 'beta' of github.com:sampotts/plyr into beta
# Conflicts:
#	dist/plyr.js
#	dist/plyr.js.map
2018-02-05 21:28:16 +11:00
1ee88cba16 Testing fetch 2018-02-05 21:26:18 +11:00
af3ae75229 Update readme.md 2018-02-03 23:16:52 +11:00
d76ef3ff91 Small UI tweaks and fix for instanceof issue 2018-01-31 19:33:00 +11:00
2691c7c9d6 Version bump + icon fix 2018-01-30 13:01:05 +11:00
26b1d8ce8f Fix UMD stuff 2018-01-30 12:57:19 +11:00
6fae148fc1 Deploy 2018-01-30 09:26:09 +11:00
bb51647fe2 Merge pull request #772 from sampotts/fix/ads-blocked
Fix: ads blocked and media playing before ad plays
2018-01-30 09:22:54 +11:00
71efbe7a92 Merge branch 'beta' into fix/ads-blocked
# Conflicts:
#	dist/plyr.js
#	dist/plyr.js.map
#	src/js/plugins/ads.js
#	src/js/plyr.js
2018-01-30 09:22:14 +11:00
afd695cb39 Fix typo's 2018-01-29 22:45:46 +01:00
c4eb4c97ac fix(ads): Fixes media from playing when ads are blocked 2018-01-29 22:40:08 +01:00
d5a1a7ca1c Add vue-plyr to readme 2018-01-28 23:03:05 -07:00
8f7a8940f3 Version bump 2018-01-25 22:42:52 +11:00
5e68f8c8dd Attempt to fix YouTube message error, added ads references, changes to bool 2018-01-25 22:41:30 +11:00
b4e22e2e7b Restored “Ad” in label 2018-01-24 09:06:20 +11:00
d4234da9aa Docs tweak 2018-01-23 19:13:35 +11:00
113 changed files with 35624 additions and 5745 deletions

View File

@ -5,9 +5,14 @@
"browser": true,
"es6": true
},
"globals": { "Plyr": false, "jQuery": false },
"globals": {
"Plyr": false,
"jQuery": false
},
"rules": {
"import/no-cycle": 1,
"no-const-assign": 1,
"no-shadow": 0,
"no-this-before-super": 1,
"no-undef": 1,
"no-unreachable": 1,
@ -20,19 +25,9 @@
"eqeqeq": [2, "always"],
"one-var": [2, "never"],
"comma-dangle": [2, "always-multiline"],
"no-restricted-globals": [
"error",
{
"name": "event",
"message": "Use local parameter instead."
},
{
"name": "error",
"message": "Use local parameter instead."
}
],
"array-bracket-newline": [2, { "minItems": 2 }],
"array-element-newline": [2, { "minItems": 2 }]
"spaced-comment": [2, "always"],
"no-restricted-globals": 2,
"no-param-reassign": [2, { "props": false }]
},
"parserOptions": {
"sourceType": "module"

55
.github/ISSUE_TEMPLATE/bug.md vendored Normal file
View File

@ -0,0 +1,55 @@
---
name: Bug report
about: Report an issue or unexpected behaviour with Plyr
---
<!--
Before creating the issue, please make sure that...
* You aren't getting any errors in your own code, causing the problem.
* You are using the latest version of Plyr.
* There isn't already an open issue for your problem.
* You are following the documentation correctly (https://github.com/sampotts/plyr/)
* Your problem doesn't happen if you remove Plyr and use native HTML5 media (when applicable).
For problems with autoplay, see our FAQ (https://github.com/sampotts/plyr/wiki/FAQ)
If you have multiple unrelated problems, create separate issues rather than combining them into one.
Note that leaving sections blank or being vague will make it difficult for us to troubleshoot and we may close the issue.
-->
### Expected behaviour
### Actual behaviour
### Steps to reproduce
### Environment
- Browser:
- Version:
- Operating System:
- Version:
### Console errors (if any)
### Link to where the bug is happening
<!--
This link can be either to our demo at https://plyr.io/ if the problem can be observed there, or to a code playground with a **minimal** test case that demonstrates the problem.
You can use one of our prepared templates to get started creating the test case:
* HTML5 video: https://codepen.io/pen?template=bKeqpr
* HTML5 audio: https://codepen.io/pen?template=rKLywR
* YouTube: https://codepen.io/pen?template=GGqbbJ
* Vimeo: https://codepen.io/pen?template=bKeXNq
* Dash.js integration: https://codepen.io/pen?template=zaBgBy
* Hls.js integration: https://codepen.io/pen?template=oyLKQb
* Shaka Player integration: https://codepen.io/pen?template=ZRpzZO
It's important that you keep the issue description and replication demo **minimal**. If your replication includes frameworks, libraries or customizations, this makes it much harder to understand the problem and find the bug. For more help on how to create the demo, see https://github.com/sampotts/plyr/wiki/Writing-helpful-issue-descriptions
-->

10
.github/ISSUE_TEMPLATE/feature.md vendored Normal file
View File

@ -0,0 +1,10 @@
---
name: New feature
about: Request new functionality
---
<!--
Please describe the behaviour that you want to add, and why. Be as clear as possible to avoid confusion.
If you want to request multiple features that aren't directly related, then create one issue per feature.
-->

10
.github/ISSUE_TEMPLATE/improvement.md vendored Normal file
View File

@ -0,0 +1,10 @@
---
name: Improvement
about: Request a change that isn't a bug or new feature
---
<!--
Please describe the behaviour that you want to change, and why. Be as clear as possible to avoid confusion.
If you want to request multiple changes that aren't directly related, then create one issue per change.
-->

View File

@ -1,17 +1,3 @@
<!---
Please use this issue template as it makes replicating and fixing the issue easier!
--->
PLEASE USE OUR SPECIFIC ISSUE TEMPLATES for bug reports, features and improvement suggestions.
### Expected behaviour
### Actual behaviour
### Environment
- Browser:
- Version:
- Operating System:
- Version:
### Steps to reproduce
-
Our issue tracker is not for support questions. If you need help, follow our support instructions: https://github.com/sampotts/plyr/blob/master/contributing.md#support

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 (`/dist` changes) from the PR
- [ ] Test on [supported browsers](https://github.com/sampotts/plyr#browser-support)

6
.gitignore vendored
View File

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

11
.npmignore Normal file
View File

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

View File

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

5
.stickler.yml Normal file
View File

@ -0,0 +1,5 @@
linters:
eslint:
files:
ignore:
- 'node_modules/*'

View File

@ -1,6 +1,6 @@
{
"plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"],
"extends": ["stylelint-config-sass-guidelines", "stylelint-config-prettier"],
"extends": ["stylelint-config-recommended", "stylelint-config-sass-guidelines", "stylelint-config-prettier"],
"rules": {
"selector-class-pattern": null,
"selector-no-qualifying-type": [
@ -9,6 +9,7 @@
"ignore": ["attribute", "class"]
}
],
"string-no-newline": null,
"indentation": 4,
"string-quotes": "single",
"max-nesting-depth": 2,

8
.travis.yml Normal file
View File

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

5
.travis/omit-dist.sh Executable file
View File

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

5
.travis/prevent-base-master.sh Executable file
View File

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

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost/dev/plyr/demo",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@ -1,33 +0,0 @@
{
"name": "plyr",
"description": "A simple HTML5 media player using custom controls",
"homepage": "http://plyr.io",
"keywords": [
"Audio",
"Video",
"HTML5 Audio",
"HTML5 Video"
],
"authors": [
"Sam Potts <sam@potts.es>"
],
"dependencies": {},
"main": [
"dist/plyr.css",
"dist/plyr.js",
"dist/plyr.svg",
"src/less/plyr.less",
"src/scss/plyr.scss",
"src/js/plyr.js"
],
"ignore": [
"node_modules",
"bower_components",
".gitignore"
],
"repository": {
"type": "git",
"url": "git://github.com/sampotts/plyr.git"
},
"license": "MIT"
}

View File

@ -4,13 +4,14 @@
"plyr.css": "src/sass/plyr.scss"
},
"js": {
"plyr.js": "src/js/plyr.js"
"plyr.js": "src/js/plyr.js",
"plyr.polyfilled.js": "src/js/plyr.polyfilled.js"
}
},
"demo": {
"sass": {
"demo.css": "demo/src/sass/bundles/demo.scss",
"error.css": "demo/src/sass/bundles/error.csss"
"error.css": "demo/src/sass/bundles/error.scss"
},
"js": {
"demo.js": "demo/src/js/demo.js"

File diff suppressed because it is too large Load Diff

40
contributing.md Normal file
View File

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

View File

@ -1,27 +1,75 @@
# Controls
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.
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:
## Internationalization using default controls
- `Array` of options (this builds the default controls based on your choices)
- `Element` with the controls
- `String` containing the desired HTML
- `false` (or empty string or array) to disable all controls
- `Function` that will be executed and should return one of the above
## Using default controls
If you want to use the standard controls as they are, you don't need to pass any options. If you want to turn on off controls, here's the full list:
```javascript
controls: [
'play-large', // The large play button in the center
'restart', // Restart playback
'rewind', // Rewind by the seek time (default 10 seconds)
'play', // Play/pause playback
'fast-forward', // Fast forward by the seek time (default 10 seconds)
'progress', // The progress bar and scrubber for playback and buffering
'current-time', // The current time of playback
'duration', // The full duration of the media
'mute', // Toggle mute
'volume', // Volume control
'captions', // Toggle captions
'settings', // Settings menu
'pip', // Picture-in-picture (currently Safari only)
'airplay', // Airplay (currently Safari only)
'fullscreen', // Toggle fullscreen
];
```
### Internationalization using default controls
You can provide an `i18n` object as one of your options when initialising the plugin which we be used when rendering the controls.
### Example
#### Example
```javascript
i18n: {
restart: "Restart",
rewind: "Rewind {seektime} secs",
play: "Play",
pause: "Pause",
forward: "Forward {seektime} secs",
buffered: "buffered",
currentTime: "Current time",
duration: "Duration",
volume: "Volume",
toggleMute: "Toggle Mute",
toggleCaptions: "Toggle Captions",
toggleFullscreen: "Toggle Fullscreen"
restart: 'Restart',
rewind: 'Rewind {seektime} secs',
play: 'Play',
pause: 'Pause',
fastForward: 'Forward {seektime} secs',
seek: 'Seek',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
duration: 'Duration',
volume: 'Volume',
mute: 'Mute',
unmute: 'Unmute',
enableCaptions: 'Enable captions',
disableCaptions: 'Disable captions',
enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen',
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
loop: 'Loop',
start: 'Start',
end: 'End',
all: 'All',
reset: 'Reset',
disabled: 'Disabled',
advertisement: 'Ad',
}
```
@ -29,85 +77,77 @@ Note: `{seektime}` will be replaced with your configured seek time or the defaul
## Using custom HTML
You can specify the HTML for the controls using the `html` option.
You can specify the HTML as a `String` or your `Function` return for the controls using the `controls` option.
The classes and data attributes used in your template should match the `selectors` option.
The classes and data attributes used in your template should match the `selectors` option if you change any.
You need to add several placeholders to your html template that are replaced when rendering:
You need to add several placeholders to your HTML template that are replaced when rendering:
- `{id}` - the dynamically generated ID for the player (for form controls)
- `{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
You can include only the controls you need when specifying custom html.
### 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
### Example
This is an example `html` option with all controls.
Here's an example of custom controls markup (this is just all default controls shown).
```javascript
var controls = ["<div class='plyr__controls'>",
"<button type='button' data-plyr='restart'>",
"<svg><use xlink:href='#plyr-restart'></use></svg>",
"<span class='plyr__sr-only'>Restart</span>",
"</button>",
"<button type='button' data-plyr='rewind'>",
"<svg><use xlink:href='#plyr-rewind'></use></svg>",
"<span class='plyr__sr-only'>Rewind {seektime} secs</span>",
"</button>",
"<button type='button' data-plyr='play'>",
"<svg><use xlink:href='#plyr-play'></use></svg>",
"<span class='plyr__sr-only'>Play</span>",
"</button>",
"<button type='button' data-plyr='pause'>",
"<svg><use xlink:href='#plyr-pause'></use></svg>",
"<span class='plyr__sr-only'>Pause</span>",
"</button>",
"<button type='button' data-plyr='fast-forward'>",
"<svg><use xlink:href='#plyr-fast-forward'></use></svg>",
"<span class='plyr__sr-only'>Forward {seektime} secs</span>",
"</button>",
"<span class='plyr__progress'>",
"<label for='seek{id}' class='plyr__sr-only'>Seek</label>",
"<input id='seek{id}' class='plyr__progress--seek' type='range' min='0' max='100' step='0.1' value='0' data-plyr='seek'>",
"<progress class='plyr__progress--played' max='100' value='0' role='presentation'></progress>",
"<progress class='plyr__progress--buffer' max='100' value='0'>",
"<span>0</span>% buffered",
"</progress>",
"<span class='plyr__tooltip'>00:00</span>",
"</span>",
"<span class='plyr__time'>",
"<span class='plyr__sr-only'>Current time</span>",
"<span class='plyr__time--current'>00:00</span>",
"</span>",
"<span class='plyr__time'>",
"<span class='plyr__sr-only'>Duration</span>",
"<span class='plyr__time--duration'>00:00</span>",
"</span>",
"<button type='button' data-plyr='mute'>",
"<svg class='icon--muted'><use xlink:href='#plyr-muted'></use></svg>",
"<svg><use xlink:href='#plyr-volume'></use></svg>",
"<span class='plyr__sr-only'>Toggle Mute</span>",
"</button>",
"<span class='plyr__volume'>",
"<label for='volume{id}' class='plyr__sr-only'>Volume</label>",
"<input id='volume{id}' class='plyr__volume--input' type='range' min='0' max='10' value='5' data-plyr='volume'>",
"<progress class='plyr__volume--display' max='10' value='0' role='presentation'></progress>",
"</span>",
"<button type='button' data-plyr='captions'>",
"<svg class='icon--captions-on'><use xlink:href='#plyr-captions-on'></use></svg>",
"<svg><use xlink:href='#plyr-captions-off'></use></svg>",
"<span class='plyr__sr-only'>Toggle Captions</span>",
"</button>",
"<button type='button' data-plyr='fullscreen'>",
"<svg class='icon--exit-fullscreen'><use xlink:href='#plyr-exit-fullscreen'></use></svg>",
"<svg><use xlink:href='#plyr-enter-fullscreen'></use></svg>",
"<span class='plyr__sr-only'>Toggle Fullscreen</span>",
"</button>",
"</div>"].join("");
const controls = `
<div class="plyr__controls">
<button type="button" class="plyr__control" data-plyr="restart">
<svg role="presentation"><use xlink:href="#plyr-restart"></use></svg>
<span class="plyr__tooltip" role="tooltip">Restart</span>
</button>
<button type="button" class="plyr__control" data-plyr="rewind">
<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-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>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Play</span>
</button>
<button type="button" class="plyr__control" data-plyr="fast-forward">
<svg role="presentation"><use xlink:href="#plyr-fast-forward"></use></svg>
<span class="plyr__tooltip" role="tooltip">Forward {seektime} secs</span>
</button>
<div class="plyr__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-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">
<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" 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" 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>
<span class="label--not-pressed plyr__tooltip" role="tooltip">Enter fullscreen</span>
</button>
</div>
`;
// Setup the player
plyr.setup('.js-player', {
html: controls
});
const player = new Plyr('#player', { controls });
```

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

4369
demo/dist/demo.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

2
demo/dist/demo.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
demo/dist/demo.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

1
demo/dist/error.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -6,8 +6,14 @@
<title>Doh. Looks like something went wrong.</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Icons -->
<link rel="icon" href="https://cdn.plyr.io/static/icons/favicon.ico">
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16">
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png">
<!-- Docs styles -->
<link rel="stylesheet" href="dist/error.css">
<link rel="stylesheet" href="dist/error.css?v=2">
<!-- Preload -->
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">

View File

@ -27,7 +27,7 @@
<meta name="twitter:card" content="summary_large_image">
<!-- Docs styles -->
<link rel="stylesheet" href="dist/demo.css">
<link rel="stylesheet" href="dist/demo.css?v=2">
<!-- Preload -->
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
@ -65,8 +65,11 @@
</svg>Vimeo</button>
</p>
<p>Advertisement service provided by
<a href="https://vi.ai" target="_blank">vi.ai</a>
<p>Premium video monitization from
<a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border">
<img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi">
<span class="sr-only">ai.vi</span>
</a>
</p>
<div class="call-to-action">
@ -88,19 +91,22 @@
</header>
<main>
<video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
<!-- Video files -->
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4" type="video/mp4">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.webm" type="video/webm">
<div id="container">
<video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
<!-- Video files -->
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
<!-- Text track file -->
<track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
default>
<track kind="captions" label="Français" srclang="fr" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
<!-- Caption files -->
<track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
default>
<track kind="captions" label="Français" srclang="fr" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
<!-- Fallback for browsers that don't support the <video> element -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4" download>Download</a>
</video>
<!-- Fallback for browsers that don't support the <video> element -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
</video>
</div>
<ul>
<li class="plyr__cite plyr__cite--video" hidden>
@ -109,7 +115,7 @@
<title>HTML5</title>
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
</svg>
<a href="http://viewfromabluemoon.com/" target="_blank">View From A Blue Moon</a> &copy; Brainfarm
<a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank">View From A Blue Moon</a> &copy; Brainfarm
</small>
</li>
<li class="plyr__cite plyr__cite--audio" hidden>
@ -136,7 +142,7 @@
</li>
<li class="plyr__cite plyr__cite--vimeo" hidden>
<small>
<a href="https://vimeo.com/ondemand/viewfromabluemoon4k" target="_blank">View From A Blue Moon</a> on&nbsp;
<a href="https://vimeo.com/76979871" target="_blank">The New Vimeo Player</a> on&nbsp;
<span class="color--vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
@ -160,25 +166,26 @@
c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"></path>
</svg>
<p>If you think Plyr's good,
<a href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&url=http%3A%2F%2Fplyr.io&via=Sam_Potts"
target="_blank" data-shr-network="twitter">tweet it</a>
<a href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts"
target="_blank" data-shr-network="twitter">tweet it</a> 👍
</p>
</aside>
<!-- Polyfills -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,fetch"></script>
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL"
crossorigin="anonymous"></script>
<!-- Plyr core script -->
<script src="../dist/plyr.js"></script>
<script src="../dist/plyr.js" crossorigin="anonymous"></script>
<!-- Sharing libary (https://shr.one) -->
<script src="https://cdn.shr.one/1.0.1/shr.js"></script>
<script src="https://cdn.shr.one/1.0.1/shr.js" crossorigin="anonymous"></script>
<!-- Rangetouch to fix <input type="range"> on touch devices (see https://rangetouch.com) -->
<script src="https://cdn.rangetouch.com/1.0.1/rangetouch.js" async></script>
<script src="https://cdn.rangetouch.com/1.0.1/rangetouch.js" async crossorigin="anonymous"></script>
<!-- Docs script -->
<script src="dist/demo.js"></script>
<script src="dist/demo.js" crossorigin="anonymous"></script>
</body>
</html>

View File

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

View File

@ -4,246 +4,299 @@
// Please see readme.md in the root or github.com/sampotts/plyr
// ==========================================================================
document.addEventListener('DOMContentLoaded', () => {
if (window.shr) {
window.shr.setup({
count: {
classname: 'button__count',
},
});
}
import Raven from 'raven-js';
// Setup tab focus
const tabClassName = 'tab-focus';
// Remove class on blur
document.addEventListener('focusout', event => {
event.target.classList.remove(tabClassName);
});
// Add classname to tabbed elements
document.addEventListener('keydown', event => {
if (event.keyCode !== 9) {
return;
}
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
window.setTimeout(() => {
document.activeElement.classList.add(tabClassName);
}, 0);
});
// Setup the player
const player = new window.Plyr('#player', {
debug: true,
title: 'View From A Blue Moon',
iconUrl: '../dist/plyr.svg',
keyboard: {
global: true,
},
tooltips: {
controls: true,
},
captions: {
active: true,
},
keys: {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: {
tag:
'https://go.aniview.com/api/adserver6/vast/?AV_PUBLISHERID=58c25bb0073ef448b1087ad6&AV_CHANNELID=5a0458dc28a06145e4519d21&AV_URL=127.0.0.1:3000&cb=1&AV_WIDTH=640&AV_HEIGHT=480',
// Test tags
// tag: 'https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=',
// tag: 'https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator=',
},
});
// Expose for testing
window.player = player;
// Setup type toggle
const buttons = document.querySelectorAll('[data-source]');
const types = {
video: 'video',
audio: 'audio',
youtube: 'youtube',
vimeo: 'vimeo',
(() => {
const { host } = window.location;
const env = {
prod: host === 'plyr.io',
dev: host === 'dev.plyr.io',
};
let currentType = window.location.hash.replace('#', '');
const historySupport = window.history && window.history.pushState;
// Toggle class on an element
function toggleClass(element, className, state) {
if (element) {
element.classList[state ? 'add' : 'remove'](className);
}
}
document.addEventListener('DOMContentLoaded', () => {
Raven.context(() => {
const selector = '#player';
const container = document.getElementById('container');
// Set a new source
function newSource(type, init) {
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
if (!(type in types) || (!init && type === currentType) || (!currentType.length && type === types.video)) {
return;
}
if (window.shr) {
window.shr.setup({
count: {
classname: 'button__count',
},
});
}
switch (type) {
case types.video:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4',
type: 'video/mp4',
}],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
};
// Setup tab focus
const tabClassName = 'tab-focus';
break;
// Remove class on blur
document.addEventListener('focusout', event => {
if (!event.target.classList || container.contains(event.target)) {
return;
}
case types.audio:
player.source = {
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3',
},
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg',
},
],
};
event.target.classList.remove(tabClassName);
});
break;
// Add classname to tabbed elements
document.addEventListener('keydown', event => {
if (event.keyCode !== 9) {
return;
}
case types.youtube:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
}],
};
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
const focused = document.activeElement;
break;
if (!focused || !focused.classList || container.contains(focused)) {
return;
}
case types.vimeo:
player.source = {
type: 'video',
sources: [{
src: 'https://vimeo.com/76979871',
provider: 'vimeo',
}],
};
focused.classList.add(tabClassName);
}, 10);
});
break;
// Setup the player
const player = new Plyr(selector, {
debug: true,
title: 'View From A Blue Moon',
iconUrl: '../dist/plyr.svg',
keyboard: {
global: true,
},
tooltips: {
controls: true,
},
captions: {
active: true,
},
keys: {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: {
enabled: env.prod || env.dev,
publisherId: '918848828995742',
},
});
default:
break;
}
// Expose for tinkering in the console
window.player = player;
// Set the current type for next time
currentType = type;
// Setup type toggle
const buttons = document.querySelectorAll('[data-source]');
const types = {
video: 'video',
audio: 'audio',
youtube: 'youtube',
vimeo: 'vimeo',
};
let currentType = window.location.hash.replace('#', '');
const historySupport = window.history && window.history.pushState;
// Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
// Toggle class on an element
function toggleClass(element, className, state) {
if (element) {
element.classList[state ? 'add' : 'remove'](className);
}
}
// Set active on parent
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
// Set a new source
function newSource(type, init) {
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
if (
!(type in types) ||
(!init && type === currentType) ||
(!currentType.length && type === types.video)
) {
return;
}
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
cite.setAttribute('hidden', '');
});
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
}
switch (type) {
case types.video:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
type: 'video/mp4',
size: 576,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
type: 'video/mp4',
size: 720,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
type: 'video/mp4',
size: 1080,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
type: 'video/mp4',
size: 1440,
},
],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
};
// Bind to each button
Array.from(buttons).forEach(button => {
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
break;
newSource(type);
case types.audio:
player.source = {
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3',
},
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg',
},
],
};
break;
case types.youtube:
player.source = {
type: 'video',
sources: [
{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
},
],
};
break;
case types.vimeo:
player.source = {
type: 'video',
sources: [
{
src: 'https://vimeo.com/76979871',
provider: 'vimeo',
},
],
};
break;
default:
break;
}
// Set the current type for next time
currentType = type;
// Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
// Set active on parent
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
cite.setAttribute('hidden', '');
});
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
}
// Bind to each button
Array.from(buttons).forEach(button => {
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
newSource(type);
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
}
});
});
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && 'type' in event.state) {
newSource(event.state.type);
}
});
// On load
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
const video = !currentType.length;
// If there's no current type set, assume video
if (video) {
currentType = types.video;
}
// Replace current history state
if (currentType in types) {
window.history.replaceState(
{
type: currentType,
},
'',
video ? '' : `#${currentType}`,
);
}
// If it's not video, load the source
if (currentType !== types.video) {
newSource(currentType, true);
}
}
});
});
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && 'type' in event.state) {
newSource(event.state.type);
}
});
// On load
if (historySupport) {
const video = !currentType.length;
// If there's no current type set, assume video
if (video) {
currentType = types.video;
}
// Replace current history state
if (currentType in types) {
window.history.replaceState(
{
type: currentType,
},
'',
video ? '' : `#${currentType}`,
);
}
// If it's not video, load the source
if (currentType !== types.video) {
newSource(currentType, true);
}
// Raven / Sentry
// For demo site (https://plyr.io) only
if (env.prod) {
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
}
});
// Google analytics
// For demo site (https://plyr.io) only
/* eslint-disable */
if (window.location.host === 'plyr.io') {
(function(i, s, o, g, r, a, m) {
i.GoogleAnalyticsObject = r;
i[r] =
i[r] ||
function() {
(i[r].q = i[r].q || []).push(arguments);
};
i[r].l = 1 * new Date();
a = s.createElement(o);
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m);
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
window.ga('create', 'UA-40881672-11', 'auto');
window.ga('send', 'pageview');
}
/* eslint-enable */
// Google analytics
// For demo site (https://plyr.io) only
/* eslint-disable */
if (env.prod) {
((i, s, o, g, r, a, m) => {
i.GoogleAnalyticsObject = r;
i[r] =
i[r] ||
function() {
(i[r].q = i[r].q || []).push(arguments);
};
i[r].l = 1 * new Date();
a = s.createElement(o);
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m);
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
window.ga('create', 'UA-40881672-11', 'auto');
window.ga('send', 'pageview');
}
/* eslint-enable */
})();

View File

@ -34,8 +34,13 @@
@import '../components/icons';
@import '../components/links';
@import '../components/lists';
@import '../components/media';
@import '../components/navigation';
@import '../components/players';
// Plyr
@import '../../../../src/sass/plyr';
// Utils
@import '../utilities/cosmetic';
@import '../utilities/hidden';

View File

@ -3,12 +3,6 @@
// ==========================================================================
@charset 'UTF-8';
// Libs
@import '../lib/fontface';
@import '../lib/mixins';
@import '../lib/normalize';
@import '../lib/reset';
// Settings
@import '../settings/colors';
@import '../settings/cosmetic';
@ -17,6 +11,12 @@
@import '../settings/spacing';
@import '../settings/type';
// Libs
@import '../lib/fontface';
@import '../lib/mixins';
@import '../lib/normalize';
@import '../lib/reset';
// Layout
@import '../layout/error';

View File

@ -6,11 +6,11 @@
.button,
.button__count {
align-items: center;
background: #fff;
background: $color-button-background;
border: 0;
border-radius: $border-radius-base;
box-shadow: 0 1px 1px rgba(#000, 0.1);
color: $gray;
color: $color-button-text;
display: inline-flex;
padding: ($spacing-base * 0.75);
position: relative;
@ -71,7 +71,7 @@
&::before {
border: $arrow-size solid transparent;
border-left-width: 0;
border-right-color: #fff;
border-right-color: $color-button-background;
content: '';
height: 0;
position: absolute;

View File

@ -42,4 +42,8 @@ a {
&.tab-focus {
@include tab-focus();
}
&.no-border::after {
display: none;
}
}

View File

@ -1,11 +1,10 @@
// ==========================================================================
// Color
// Basic media
// ==========================================================================
.color--vimeo {
color: $color-vimeo;
}
.color--youtube {
color: $color-youtube;
img,
video,
audio {
max-width: 100%;
vertical-align: middle;
}

View File

@ -29,6 +29,7 @@ video {
position: absolute;
right: 0;
top: 0;
z-index: 3;
}
// Style full supported player

View File

@ -28,6 +28,7 @@ body {
main {
margin: auto;
padding-bottom: 1px; // Collapsing margins
text-align: center;
}

View File

@ -24,5 +24,9 @@ $color-vimeo: #19b7ed;
$color-link: #fff;
$color-background: $color-brand-primary;
// Buttons
$color-button-background: #fff;
$color-button-text: $gray;
// Focus
$tab-focus-default-color: #fff;

View File

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

View File

@ -16,3 +16,4 @@ $plyr-font-size-captions-base: $plyr-font-size-base;
$plyr-font-size-captions-small: $plyr-font-size-small;
$plyr-font-size-captions-medium: 18px;
$plyr-font-size-captions-large: 21px;
$plyr-font-size-menu: $plyr-font-size-base;

View File

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

View File

@ -6,5 +6,6 @@ h1 {
@include font-size($font-size-h1);
font-weight: $font-weight-bold;
letter-spacing: $letter-spacing-headings;
margin: 0 0 ($spacing-base / 2);
line-height: 1.2;
margin: 0 0 $spacing-base;
}

View File

@ -0,0 +1,7 @@
// ==========================================================================
// Misc cosmetic
// ==========================================================================
.no-border {
border: 0;
}

View File

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

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

8438
dist/plyr.js vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/plyr.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

13862
dist/plyr.polyfilled.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/plyr.polyfilled.js.map vendored Normal file

File diff suppressed because one or more lines are too long

2
dist/plyr.polyfilled.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/plyr.polyfilled.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -9,9 +9,11 @@ const path = require('path');
const gulp = require('gulp');
const gutil = require('gulp-util');
const concat = require('gulp-concat');
const filter = require('gulp-filter');
const sass = require('gulp-sass');
const cleancss = require('gulp-clean-css');
const run = require('run-sequence');
const header = require('gulp-header');
const prefix = require('gulp-autoprefixer');
const gitbranch = require('git-branch');
const svgstore = require('gulp-svgstore');
@ -24,21 +26,16 @@ const size = require('gulp-size');
const rollup = require('gulp-better-rollup');
const babel = require('rollup-plugin-babel');
const sourcemaps = require('gulp-sourcemaps');
const uglify = require('rollup-plugin-uglify');
const { minify } = require('uglify-es');
const uglify = require('gulp-uglify-es').default;
const commonjs = require('rollup-plugin-commonjs');
const resolve = require('rollup-plugin-node-resolve');
const FastlyPurge = require('fastly-purge');
const through = require('through2');
const bundles = require('./bundles.json');
const pkg = require('./package.json');
// Get AWS config
let aws = {};
try {
aws = require('./aws.json'); //eslint-disable-line
} catch (e) {
// Do nothing
}
const minSuffix = '.min';
// Paths
const root = __dirname;
@ -47,7 +44,7 @@ const paths = {
// Source paths
src: {
sass: path.join(root, 'src/sass/**/*.scss'),
js: path.join(root, 'src/js/**/*'),
js: path.join(root, 'src/js/**/*.js'),
sprite: path.join(root, 'src/sprite/*.svg'),
},
@ -58,7 +55,7 @@ const paths = {
// Source paths
src: {
sass: path.join(root, 'demo/src/sass/**/*.scss'),
js: path.join(root, 'demo/src/js/**/*'),
js: path.join(root, 'demo/src/js/**/*.js'),
},
// Output paths
@ -68,8 +65,11 @@ const paths = {
root: path.join(root, 'demo/'),
},
upload: [
path.join(root, 'dist/**'),
path.join(root, 'demo/dist/**'),
path.join(root, `dist/*${minSuffix}.*`),
path.join(root, 'dist/*.css'),
path.join(root, 'dist/*.svg'),
path.join(root, `demo/dist/*${minSuffix}.*`),
path.join(root, 'demo/dist/*.css'),
],
};
@ -89,16 +89,18 @@ const browsers = ['> 1%'];
// Babel config
const babelrc = {
presets: [[
'env',
{
targets: {
browsers,
presets: [
[
'env',
{
targets: {
browsers,
},
useBuiltIns: true,
modules: false,
},
useBuiltIns: true,
modules: false,
},
]],
],
],
plugins: ['external-helpers'],
babelrc: false,
exclude: 'node_modules/**',
@ -106,10 +108,9 @@ const babelrc = {
// Clean out /dist
gulp.task('clean', () => {
const dirs = [
paths.plyr.output,
paths.demo.output,
].map(dir => path.join(dir, '**/*'));
const dirs = [paths.plyr.output, paths.demo.output].map(dir =>
path.join(dir, '**/*'),
);
// Don't delete the mp4
dirs.push(`!${path.join(paths.plyr.output, '**/*.mp4')}`);
@ -122,8 +123,9 @@ const build = {
Object.keys(files).forEach(key => {
const name = `js:${key}`;
tasks.js.push(name);
const { output } = paths[bundle];
gulp.task(name, () =>
return gulp.task(name, () =>
gulp
.src(bundles[bundle].js[key])
.pipe(sourcemaps.init())
@ -135,15 +137,20 @@ const build = {
resolve(),
commonjs(),
babel(babelrc),
uglify({}, minify),
],
},
options,
),
)
.pipe(size(sizeOptions))
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(paths[bundle].output)),
.pipe(gulp.dest(output))
.pipe(filter('**/*.js'))
.pipe(uglify())
.pipe(size(sizeOptions))
.pipe(rename({ suffix: minSuffix }))
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(output)),
);
});
},
@ -152,7 +159,7 @@ const build = {
const name = `sass:${key}`;
tasks.sass.push(name);
gulp.task(name, () =>
return gulp.task(name, () =>
gulp
.src(bundles[bundle].sass[key])
.pipe(sass())
@ -170,14 +177,16 @@ const build = {
tasks.sprite.push(name);
// Process Icons
gulp.task(name, () =>
return gulp.task(name, () =>
gulp
.src(paths[bundle].src.sprite)
.pipe(
svgmin({
plugins: [{
removeDesc: true,
}],
plugins: [
{
removeDesc: true,
},
],
}),
)
.pipe(svgstore())
@ -214,27 +223,40 @@ 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
// --------------------------------------------
// If aws is setup
if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
// Get deployment config
let credentials = {};
try {
credentials = require('./credentials.json'); //eslint-disable-line
} catch (e) {
// Do nothing
}
// If deployment is setup
if (
Object.keys(credentials).includes('aws') &&
Object.keys(credentials).includes('fastly')
) {
const { version } = pkg;
const { aws, fastly } = credentials;
// Get branch info
const branch = {
current: gitbranch.sync(),
master: 'master',
beta: 'beta',
develop: 'develop',
};
const allowed = [
branch.master,
branch.beta,
];
const maxAge = 31536000; // 1 year
const options = {
@ -245,9 +267,10 @@ 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',
'Cache-Control':
'no-cache, no-store, must-revalidate, max-age=0',
Vary: 'Accept-Encoding',
},
},
@ -256,69 +279,132 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
headers: {
// http://stackoverflow.com/questions/2272835/amazon-s3-object-redirect
'x-amz-website-redirect-location': `/${ver}/${filename}`,
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
'Cache-Control':
'no-cache, no-store, must-revalidate, max-age=0',
},
};
},
};
const regex = '(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*).(?:0|[1-9][0-9]*)(?:-[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?(?:\\+[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?';
const regex =
'(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*).(?:0|[1-9][0-9]*)(?:-[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?(?:\\+[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?';
const semver = new RegExp(`v${regex}`, 'gi');
const localPath = new RegExp('(../)?dist', 'gi');
const versionPath = `https://${aws.cdn.domain}/${version}`;
const cdnpath = new RegExp(`${aws.cdn.domain}/${regex}/`, 'gi');
const renameFile = rename(p => {
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
});
// Check we're on the correct branch to deploy
const canDeploy = () => {
const allowed = [branch.master, branch.develop];
if (!allowed.includes(branch.current)) {
console.error(
`Must be on ${allowed.join(', ')} to publish! (current: ${
branch.current
})`,
);
return false;
}
return true;
};
gulp.task('version', () => {
if (!canDeploy()) {
return null;
}
console.log(`Updating versions to '${version}'...`);
// Replace versioned URLs in source
const files = ['plyr.js', 'plyr.polyfilled.js', 'defaults.js'];
return gulp
.src(files.map(file => path.join(root, `src/js/${file}`)))
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest(path.join(root, 'src/js/')));
});
// Publish version to CDN bucket
gulp.task('cdn', () => {
if (!allowed.includes(branch.current)) {
console.error(`Must be on ${allowed.join(', ')} to publish! (current: ${branch.current})`);
if (!canDeploy()) {
return null;
}
console.log(`Uploading '${version}' to ${aws.cdn.domain}...`);
// Upload to CDN
return gulp
.src(paths.upload)
.pipe(
size({
showFiles: true,
gzip: true,
}),
)
.pipe(
rename(p => {
// eslint-disable-next-line
p.dirname = p.dirname.replace('.', version);
}),
)
.pipe(replace(localPath, versionPath))
.pipe(s3(aws.cdn, options.cdn));
return (
gulp
.src(paths.upload)
.pipe(renameFile)
// Remove min suffix from source map URL
.pipe(
replace(
/sourceMappingURL=([\w-?.]+)/,
(match, p1) =>
`sourceMappingURL=${p1.replace(minSuffix, '')}`,
),
)
.pipe(
size({
showFiles: true,
gzip: true,
}),
)
.pipe(replace(localPath, versionPath))
.pipe(s3(aws.cdn, options.cdn))
);
});
// Purge the fastly cache incase any 403/404 are cached
gulp.task('purge', () => {
const list = [];
return gulp.src(paths.upload).pipe(
through.obj((file, enc, cb) => {
const filename = file.path.split('/').pop();
list.push(`${versionPath}/${filename}`);
cb(null);
}),
).on('end', () => {
const purge = new FastlyPurge(fastly.token);
list.forEach(url => {
console.log(`Purging ${url}...`);
purge.url(url, (error, result) => {
if (error) {
console.log(error);
} else if (result) {
console.log(result);
}
});
});
});
});
// Publish to demo bucket
gulp.task('demo', () => {
if (!allowed.includes(branch.current)) {
console.error(`Must be on ${allowed.join(', ')} to publish! (current: ${branch.current})`);
if (!canDeploy()) {
return null;
}
console.log(`Uploading '${version}' demo to ${aws.demo.domain}...`);
const cdnpath = new RegExp(`${aws.cdn.domain}/${regex}/`, 'gi');
// Replace versioned files in readme.md
gulp
.src([`${root}/readme.md`])
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest(root));
// Replace versioned files in plyr.js
gulp
.src(path.join(root, 'src/js/plyr.js'))
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest(path.join(root, 'src/js/')));
// Replace local file paths with remote paths in demo HTML
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
const index = `${paths.demo.root}index.html`;
@ -369,22 +455,29 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
}));
}); */
// Open the demo site to check it's sweet
gulp.task('open', () => {
console.log(`Opening ${aws.demo.domain}...`);
// A file must be specified or gulp will skip the task
// Doesn't matter which file since we set the URL above
// Weird, I know...
return gulp.src([`${paths.demo.root}index.html`]).pipe(
open('', {
url: `http://${aws.demo.domain}`,
// Open the demo site to check it's ok
gulp.task('open', callback => {
gulp.src(__filename).pipe(
open({
uri: `https://${aws.demo.domain}`,
}),
);
callback();
});
// Do everything
gulp.task('publish', () => {
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo');
});
gulp.task('deploy', () =>
run(
'version',
tasks.clean,
tasks.js,
tasks.sass,
tasks.sprite,
'cdn',
'purge',
'demo',
'open',
),
);
}

View File

@ -1,57 +1,19 @@
{
"name": "plyr",
"version": "3.0.0-beta.6",
"version": "3.4.3",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"main": "./dist",
"author": "Sam Potts <sam@potts.es>",
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
"main": "./dist/plyr.js",
"browser": "./dist/plyr.min.js",
"sass": "./src/sass/plyr.scss",
"style": "./dist/plyr.css",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.1",
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.6.1",
"del": "^3.0.0",
"eslint": "^4.16.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.8.0",
"git-branch": "^1.0.0",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^4.1.0",
"gulp-better-rollup": "^3.0.0",
"gulp-clean-css": "^3.9.2",
"gulp-concat": "^2.6.1",
"gulp-open": "^2.1.0",
"gulp-rename": "^1.2.2",
"gulp-replace": "^0.6.1",
"gulp-s3": "^0.11.0",
"gulp-sass": "^3.1.0",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^1.12.1",
"gulp-svgmin": "^1.2.4",
"gulp-svgstore": "^6.1.1",
"gulp-util": "^3.0.8",
"rollup-plugin-babel": "^3.0.3",
"rollup-plugin-commonjs": "^8.2.6",
"rollup-plugin-node-resolve": "^3.0.2",
"rollup-plugin-uglify": "^3.0.0",
"run-sequence": "^2.2.1",
"stylelint": "^8.4.0",
"stylelint-config-prettier": "^2.0.0",
"stylelint-config-sass-guidelines": "^4.1.0",
"stylelint-config-standard": "^18.0.0",
"stylelint-order": "^0.8.0",
"stylelint-scss": "^2.2.0",
"stylelint-selector-bem-pattern": "^2.0.0",
"uglify-es": "^3.3.8"
},
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
"license": "MIT",
"repository": {
"type": "git",
"url": "git://github.com/sampotts/plyr.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/sampotts/plyr/issues"
},
@ -59,8 +21,65 @@
"doc": "readme.md"
},
"scripts": {
"build": "gulp build",
"lint": "eslint src/js && npm run-script remark",
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Sam Potts <sam@potts.es>",
"dependencies": {}
"devDependencies": {
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.6",
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.7.0",
"del": "^3.0.0",
"eslint": "^5.3.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^3.0.1",
"eslint-plugin-import": "^2.14.0",
"fastly-purge": "^1.0.1",
"git-branch": "^2.0.1",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^5.0.0",
"gulp-better-rollup": "^3.3.0",
"gulp-clean-css": "^3.10.0",
"gulp-concat": "^2.6.1",
"gulp-filter": "^5.1.0",
"gulp-header": "^2.0.5",
"gulp-open": "^3.0.1",
"gulp-postcss": "^8.0.0",
"gulp-rename": "^1.4.0",
"gulp-replace": "^1.0.0",
"gulp-s3": "^0.11.0",
"gulp-sass": "^4.0.1",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.4",
"gulp-svgmin": "^1.2.4",
"gulp-svgstore": "^6.1.1",
"gulp-uglify-es": "^1.0.4",
"gulp-util": "^3.0.8",
"postcss-custom-properties": "^7.0.0",
"prettier-eslint": "^8.8.2",
"prettier-stylelint": "^0.4.2",
"remark-cli": "^5.0.0",
"remark-validate-links": "^7.1.0",
"rollup-plugin-babel": "^3.0.7",
"rollup-plugin-commonjs": "^9.1.5",
"rollup-plugin-node-resolve": "^3.3.0",
"run-sequence": "^2.2.1",
"stylelint": "^9.4.0",
"stylelint-config-prettier": "^4.0.0",
"stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.0.0",
"stylelint-order": "^1.0.0",
"stylelint-scss": "^3.3.0",
"stylelint-selector-bem-pattern": "^2.0.0",
"through2": "^2.0.3"
},
"dependencies": {
"babel-polyfill": "^6.26.0",
"custom-event-polyfill": "^1.0.6",
"loadjs": "^3.5.4",
"raven-js": "^3.26.4",
"url-polyfill": "^1.0.14"
}
}

View File

@ -11,7 +11,8 @@
},
// Exclude from search
"search.exclude": {
"dist/": true
"dist/": true,
"demo/dist/": true
},
// Linting
"stylelint.enable": true,

472
readme.md
View File

@ -1,33 +1,33 @@
---
Beware: This branch is currently in beta and not production-ready
---
# Plyr
A simple, lightweight, accessible and customizable HTML5, YouTube and Vimeo media player that supports [_modern_](#browser-support) browsers.
[Checkout the demo](https://plyr.io) - [Donate to support Plyr](#donate)
[Checkout the demo](https://plyr.io) - [Donate to support Plyr](#donate) - [Chat on Slack](https://bit.ly/plyr-chat)
[![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png)](https://plyr.io)
[![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png?v=3)](https://plyr.io)
## Features
* **Accessible** - full support for VTT captions and screen readers
* **Lightweight** - just 18KB minified and gzipped
* **[Customisable](#html)** - make the player look how you want with the markup you want
* **Semantic** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
`<span>` or `<a href="#">` button hacks
* **Responsive** - works with any screen size
* **HTML Video & Audio** - support for both formats
* **[Embedded Video](#embeds)** - support for YouTube and Vimeo video playback
* **[Streaming](#streaming)** - support for hls.js, Shaka and dash.js streaming playback
* **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
* **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats
* **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
* **[Shortcuts](#shortcuts)** - supports keyboard shortcuts
* **i18n support** - support for internationalization of controls
* **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required
* **SASS** - to include in your build processes
- **Accessible** - full support for VTT captions and screen readers
- **[Customisable](#html)** - make the player look how you want with the markup you want
- **Good HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
`<span>` or `<a href="#">` button hacks
- **Responsive** - works with any screen size
- **HTML Video & Audio** - support for both formats
- **[Embedded Video](#embeds)** - support for YouTube and Vimeo video playback
- **[Monetization](#ads)** - make money from your videos
- **[Streaming](#try-plyr-online)** - support for hls.js, Shaka and dash.js streaming playback
- **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
- **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats
- **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
- **[Shortcuts](#shortcuts)** - supports keyboard shortcuts
- **Picture-in-Picture** - supports Safari's picture-in-picture mode
- **Playsinline** - supports the `playsinline` attribute
- **Speed controls** - adjust speed on the fly
- **Multiple captions** - support for multiple caption tracks
- **i18n support** - support for internationalization of controls
- **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required
- **SASS** - to include in your build processes
Oh and yes, it works with Bootstrap.
@ -35,40 +35,29 @@ Oh and yes, it works with Bootstrap.
Check out the [changelog](changelog.md) to see what's new with Plyr.
## CMS plugins
## Plugins & Components
### [WordPress](https://wordpress.org/plugins/plyr/)
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
Created and maintained by Ryan Anthony Drake ([@iamryandrake](https://github.com/iamryandrake))
### [Neos](https://packagist.org/packages/jonnitto/plyr)
Created and maintained by Jon Uhlmann ([@jonnitto](https://github.com/jonnitto))
### [Kirby](https://github.com/dpschen/kirby-plyrtag)
Created and maintained by Dominik Pschenitschni ([@dpschen](https://github.com/dpschen))
## Using package managers
You can grab the source using one of the following package managers.
### npm
```
npm install plyr
```
[https://www.npmjs.com/package/plyr](https://www.npmjs.com/package/plyr)
| Type | Maintainer | Link |
| --------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
| React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) |
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
| Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
## Quick setup
Here's a quick run through on getting up and running. There's also a [demo on Codepen](http://codepen.io/sampotts/pen/jARJYp).
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 markup so that's all you need for those types. More info on advanced HTML markup can be found under
[initialising](#initialising).
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.
#### HTML5 Video
@ -91,38 +80,65 @@ Plyr extends upon the standard HTML5 markup so that's all you need for those typ
</audio>
```
For YouTube and Vimeo players, Plyr uses progressive enhancement to enhance the default `<iframe>` embeds. Below are some examples. The `plyr__video-embed` classname will make the embed responsive. You can add the `autoplay`, `loop` and `playsinline` (YouTube only) query parameters to the URL and they will be set as config options automatically. For YouTube, the `origin` should be updated to reflect the domain you're hosting the embed on, or you can opt to omit it.
For YouTube and Vimeo players, Plyr uses progressive enhancement to enhance the default `<iframe>` embeds. Below are some examples. The `plyr__video-embed` classname will make the embed responsive. You can add the `autoplay`, `loop`, `hl` (YouTube only) and `playsinline` (YouTube only) query parameters to the URL and they will be set as config options automatically. For YouTube, the `origin` should be updated to reflect the domain you're hosting the embed on, or you can opt to omit it.
#### YouTube embed
We recommend [progressive enhancement](https://www.smashingmagazine.com/2009/04/progressive-enhancement-what-it-is-and-how-to-use-it/) with the embedded players. You can elect to use an `<iframe>` as the source element (which Plyr will progressively enhance) or a bog standard `<div>` with two essential data attributes - `data-plyr-provider` and `data-plyr-embed-id`.
```html
<div class="plyr__video-embed" id="player">
<iframe src="https://www.youtube.com/embed/bTqVqk7FSmY?origin=https://plyr.io&amp;iv_load_policy=3&amp;modestbranding=1&amp;playsinline=1&amp;showinfo=0&amp;rel=0&amp;enablejsapi=1" allowfullscreen allowtransparency allow="autoplay"></iframe>
</div>
```
_Note_: The `plyr__video-embed` classname will make the player a responsive 16:9 (most common) iframe embed. When plyr itself kicks in, your custom `ratio` config option will be used.
Or the `<div>` non progressively enhanced method:
```html
<div id="player" data-plyr-provider="youtube" data-plyr-embed-id="bTqVqk7FSmY"></div>
```
_Note_: The `data-plyr-embed-id` can either be the video ID or URL for the media.
#### Vimeo embed
Much the same as YouTube above.
```html
<div class="plyr__video-embed" id="player">
<iframe src="https://player.vimeo.com/video/76979871?loop=false&amp;byline=false&amp;portrait=false&amp;title=false&amp;speed=true&amp;transparent=0&amp;gesture=media" allowfullscreen allowtransparency allow="autoplay"></iframe>
</div>
```
Or the `<div>` non progressively enhanced method:
```html
<div id="player" data-plyr-provider="vimeo" data-plyr-embed-id="76979871"></div>
```
### JavaScript
Include the `plyr.js` script before the closing `</body>` tag and then call `plyr.setup()`. More info on `setup()` can be found under
[initialising](#initialising).
Include the `plyr.js` script before the closing `</body>` tag and then in your JS create a new instance of Plyr as below.
```html
<script src="path/to/plyr.js"></script>
<script>const player = new Plyr('#player');</script>
```
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following:
See [initialising](#initialising) for more information on advanced setups.
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
```html
<script src="https://cdn.plyr.io/3.0.0-beta.6/plyr.js"></script>
<script src="https://cdn.plyr.io/3.4.3/plyr.js"></script>
```
...or...
```html
<script src="https://cdn.plyr.io/3.4.3/plyr.polyfilled.js"></script>
```
### CSS
@ -136,13 +152,23 @@ 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.0.0-beta.6/plyr.css">
<link rel="stylesheet" href="https://cdn.plyr.io/3.4.3/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.0.0-beta.6/plyr.svg`.
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.4.3/plyr.svg`.
## Ads
Plyr has partnered up with [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) to offer monetization options for your videos. Getting setup is easy:
- [Sign up for a vi.ai account](https://vi.ai/publisher-video-monetization/?aid=plyrio)
- Grab your publisher ID from the code snippet
- Enable ads in the [config options](#options) and enter your publisher ID
Any questions regarding the ads can be sent straight to vi.ai and any issues with rendering raised through GitHub issues.
## Advanced
@ -187,11 +213,12 @@ WebVTT captions are supported. To add a caption track, check the HTML example ab
You can specify a range of arguments for the constructor to use:
* A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
* A [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
* A [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) or Array of [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) -
the first element will be used
* A [jQuery](https://jquery.com) object - if multiple are passed, the first element will be used
- A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
- A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
- A [`NodeList`](https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
- A [jQuery](https://jquery.com) object
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below.
Here's some examples
@ -207,15 +234,33 @@ 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.
The second argument for the constructor is the [#options](options) object:
##### 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(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
const player = new Plyr('#player', {
@ -223,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
@ -235,42 +280,43 @@ Options can be passed as an object to the constructor as above or as JSON in `da
Note the single quotes encapsulating the JSON and double quotes on the object keys. Only string values need double quotes.
| Option | Type | Default | Description |
| -------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `enabled` | Boolean | `true` | Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below. |
| `debug` | Boolean | `false` | Display debugging information in the console |
| `controls` | Function or Array | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return a string of HTML for the controls. Three arguments will be passed to your function; id (the unique id for the player), seektime (the seektime step in seconds), and title (the media title). See [controls.md](controls.md) for more info on how the html needs to be structured. |
| `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If you're using the default controls are used then you can specify which settings to show in the menu |
| `i18n` | Object | See [defaults.js](/src/js/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
| `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. |
| `iconUrl` | String | `null` | Specify a URL or path to the SVG sprite. See the [SVG section](#svg) for more info. |
| `iconPrefix` | String | `plyr` | Specify the id prefix for the icons used in the default controls (e.g. "plyr-play" would be "plyr"). This is to prevent clashes if you're using your own SVG sprite but with the default controls. Most people can ignore this option. |
| `blankUrl` | String | `https://cdn.plyr.io/static/blank.mp4` | Specify a URL or path to a blank video file used to properly cancel network requests. |
| `autoplay` | Boolean | `false` | Autoplay the media on load. This is generally advised against on UX grounds. It is also disabled by default in some browsers. If the `autoplay` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
| `autopause`&sup1; | Boolean | `true` | Only allow one player playing at once. |
| `seekTime` | Number | `10` | The time, in seconds, to seek when a user hits fast forward or rewind. |
| `volume` | Number | `1` | A number, between 0 and 1, representing the initial volume of the player. |
| `muted` | Boolean | `false` | Whether to start playback muted. If the `muted` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
| `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. |
| `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. |
| `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. |
| `showPosterOnEnd` | Boolean | false | This will restore and _reload_ HTML5 video once playback is complete. Note: depending on the browser caching, this may result in the video downloading again (or parts of it). Use with caution. |
| `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally |
| `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. |
| `duration` | Number | `null` | Specify a custom duration for media. |
| `displayDuration` | Boolean | `true` | Displays the duration of the media on the "metadataloaded" event (on startup) in the current time display. This will only work if the `preload` attribute is not set to `none` (or is not set at all) and you choose not to display the duration (see `controls` option). |
| `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). |
| `fullscreen` | Object | `{ enabled: true, fallback: true }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. |
| `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. |
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: Options to display in the menu. Most browsers will refuse to play slower than 0.5. |
| `quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display. |
| `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. |
| Option | Type | Default | Description |
| -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | Boolean | `true` | Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below. |
| `debug` | Boolean | `false` | Display debugging information in the console |
| `controls` | Array, Function or Element | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function; `id` (the unique id for the player), `seektime` (the seektime step in seconds), and `title` (the media title). See [controls.md](controls.md) for more info on how the html needs to be structured. |
| `settings` | Array | `['captions', 'quality', 'speed', 'loop']` | If you're using the default controls are used then you can specify which settings to show in the menu |
| `i18n` | Object | See [defaults.js](/src/js/config/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
| `loadSprite` | Boolean | `true` | Load the SVG sprite specified as the `iconUrl` option (if a URL). If `false`, it is assumed you are handling sprite loading yourself. |
| `iconUrl` | String | `null` | Specify a URL or path to the SVG sprite. See the [SVG section](#svg) for more info. |
| `iconPrefix` | String | `plyr` | Specify the id prefix for the icons used in the default controls (e.g. "plyr-play" would be "plyr"). This is to prevent clashes if you're using your own SVG sprite but with the default controls. Most people can ignore this option. |
| `blankVideo` | String | `https://cdn.plyr.io/static/blank.mp4` | Specify a URL or path to a blank video file used to properly cancel network requests. |
| `autoplay` | Boolean | `false` | Autoplay the media on load. This is generally advised against on UX grounds. It is also disabled by default in some browsers. If the `autoplay` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
| `autopause`&sup1; | Boolean | `true` | Only allow one player playing at once. |
| `seekTime` | Number | `10` | The time, in seconds, to seek when a user hits fast forward or rewind. |
| `volume` | Number | `1` | A number, between 0 and 1, representing the initial volume of the player. |
| `muted` | Boolean | `false` | Whether to start playback muted. If the `muted` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
| `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. |
| `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. |
| `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. |
| `resetOnEnd` | Boolean | false | Reset the playback to the start once playback is complete. |
| `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally |
| `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. |
| `duration` | Number | `null` | Specify a custom duration for media. |
| `displayDuration` | Boolean | `true` | Displays the duration of the media on the "metadataloaded" event (on startup) in the current time display. This will only work if the `preload` attribute is not set to `none` (or is not set at all) and you choose not to display the duration (see `controls` option). |
| `invertTime` | Boolean | `true` | Display the current time as a countdown rather than an incremental counter. |
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
| `captions` | Object | `{ active: false, language: 'auto', update: false }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). 'auto' uses the browser language. `update`: Listen to changes to tracks and update menu. This is needed for some streaming libraries, but can result in unselectable language options). |
| `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. |
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: Options to display in the menu. Most browsers will refuse to play slower than 0.5. |
| `quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display. |
| `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. |
| `ads` | Object | `{ enabled: false, publisherId: '' }` | `enabled`: Whether to enable vi.ai ads. `publisherId`: Your unique vi.ai publisher ID. |
1. Vimeo only
1. Vimeo only
## API
@ -296,72 +342,84 @@ element.addEventListener('ready', event => {
### Methods
Methods are not chainable. An example use of a method:
Example method use:
```javascript
player.play();
player.play(); // Start playback
player.fullscreen.enter(); // Enter fullscreen
```
| Method | Parameters | Description |
| ------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `play()`&sup1; | - | Start playback. |
| `pause()` | - | Pause playback. |
| `togglePlay(toggle)` | Boolean | Toggle playback, if no parameters are passed, it will toggle based on current status. |
| `stop()` | - | Stop playback and reset to start. |
| `restart()` | - | Restart playback. |
| `rewind(seekTime)` | Number | Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used. |
| `forward(seekTime)` | Number | Fast forward by the specified seek time. If no parameter is passed, the default seek time will be used. |
| `increaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
| `decreaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
| `toggleCaptions(toggle)` | Boolean | Toggle captions display. If no parameter is passed, it will toggle based on current status. |
| `toggleFullscreen(event)` | Event | Toggle fullscreen. Fullscreen can only be initiated by a user event. Exit is possible without user input. |
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
| `toggleControls(toggle)` | Boolean | Toggle the controls based on the specified boolean. |
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
| `supports(type)` | String | Check support for a mime type. |
| `destroy()` | - | Destroy the instance and garbage collect any elements. |
| Method | Parameters | Description |
| ------------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `play()`&sup1; | - | Start playback. |
| `pause()` | - | Pause playback. |
| `togglePlay(toggle)` | Boolean | Toggle playback, if no parameters are passed, it will toggle based on current status. |
| `stop()` | - | Stop playback and reset to start. |
| `restart()` | - | Restart playback. |
| `rewind(seekTime)` | Number | Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used. |
| `forward(seekTime)` | Number | Fast forward by the specified seek time. If no parameter is passed, the default seek time will be used. |
| `increaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
| `decreaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. |
| `toggleCaptions(toggle)` | Boolean | Toggle captions display. If no parameter is passed, it will toggle based on current status. |
| `fullscreen.enter()` | - | Enter fullscreen. If fullscreen is not supported, a fallback "full window/viewport" is used instead. |
| `fullscreen.exit()` | - | Exit fullscreen. |
| `fullscreen.toggle()` | - | Toggle fullscreen. |
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
| `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. |
1. For HTML5 players, `play()` will return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) in _some_ browsers - WebKit and Mozilla [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) at time of writing.
1. For HTML5 players, `play()` will return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) in _some_ browsers - WebKit and Mozilla [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) at time of writing.
### Getters and Setters
An example setter:
Example setters:
```javascript
player.volume = 0.5;
player.volume = 0.5; // Sets volume at 50%
player.currentTime = 10; // Seeks to 10 seconds
```
An example getter:
Example getters:
```javascript
player.volume; // returns 0.5;
player.volume; // 0.5;
player.currentTime; // 10
player.fullscreen.active; // false;
```
| Property | Getter | Setter | Description |
| --------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isHTML5` | | - | Returns a boolean indicating if the current player is HTML5. |
| `isEmbed` | | - | Returns a boolean indicating if the current player is an embedded player. |
| `paused` | | - | Returns a boolean indicating if the current player is paused. |
| `playing` | | - | Returns a boolean indicating if the current player is playing. |
| `ended` | | - | Returns a boolean indicating if the current player has finished playback. |
| `currentTime` | | | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
| `seeking` | | - | Returns a boolean indicating if the current player is seeking. |
| `duration` | | - | Returns the duration for the current media. |
| `volume` | | | Gets or sets the volume for the player. The setter accepts a float between 0 and 1. |
| `muted` | | | Gets or sets the muted state of the player. The setter accepts a boolean. |
| `hasAudio` | | - | Returns a boolean indicating if the current media has an audio track. |
| `speed` | | | Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5. |
| `quality`&sup1; | ✔ | | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
| `loop` | | | Gets or sets the current loop state of the player. The setter accepts a boolean. |
| `source` | | | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
| `poster`&sup2; | | | 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. |
| `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+. |
| Property | Getter | Setter | Description |
| -------------------- | ------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `isHTML5` | | - | Returns a boolean indicating if the current player is HTML5. |
| `isEmbed` | | - | Returns a boolean indicating if the current player is an embedded player. |
| `playing` | | - | Returns a boolean indicating if the current player is playing. |
| `paused` | | - | Returns a boolean indicating if the current player is paused. |
| `stopped` | | - | Returns a boolean indicating if the current player is stopped. |
| `ended` | | - | Returns a boolean indicating if the current player has finished playback. |
| `buffered` | | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
| `currentTime` | | | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
| `seeking` | | - | Returns a boolean indicating if the current player is seeking. |
| `duration` | | - | Returns the duration for the current media. |
| `volume` | | | Gets or sets the volume for the player. The setter accepts a float between 0 and 1. |
| `muted` | | | Gets or sets the muted state of the player. The setter accepts a boolean. |
| `hasAudio` | | - | Returns a boolean indicating if the current media has an audio track. |
| `speed` | | | Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5. |
| `quality`&sup1; | | | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
| `loop` | | | Gets or sets the current loop state of the player. The setter accepts a boolean. |
| `source` | | | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#the-source-setter) below for examples. |
| `poster` | | | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `autoplay` | | | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
| `currentTrack` | ✓ | ✓ | Gets or sets the caption track by index. `-1` means the track is missing or captions is not active |
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. If your captions don't have any language data, or if you have multiple tracks with the same language, you may want to use `currentTrack` instead. |
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
| `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. |
1. YouTube only. HTML5 will follow.
2. HTML5 only
1. YouTube only. HTML5 will follow.
2. HTML5 only
#### The `.source` setter
@ -377,10 +435,12 @@ player.source = {
{
src: '/path/to/movie.mp4',
type: 'video/mp4',
size: 720,
},
{
src: '/path/to/movie.webm',
type: 'video/webm',
size: 1080,
},
],
poster: '/path/to/poster.jpg',
@ -461,7 +521,7 @@ _Note:_ `src` property for YouTube and Vimeo can either be the video ID or the w
| `poster`&sup1; | String | The URL for the poster image (HTML5 video only). |
| `tracks`&sup1; | String | An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above, it will render as `<track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default>` and similar for the French version. Booleans are converted to HTML5 value-less attributes. |
1. HTML5 only
1. HTML5 only
## Events
@ -505,6 +565,7 @@ player.on('ready', event => {
| `loadstart` | Sent when loading of the media begins. |
| `loadeddata` | The first frame of the media has finished loading. |
| `loadedmetadata` | The media's metadata has finished loading; all attributes now contain as much useful information as they're going to. |
| `qualitychange` | The quality of playback has changed. |
| `canplay` | Sent when enough data is available that the media can be played, at least for a couple of frames. This corresponds to the `HAVE_ENOUGH_DATA` `readyState`. |
| `canplaythrough` | Sent when the ready state changes to `CAN_PLAY_THROUGH`, indicating that the entire media can be played without interruption, assuming the download rate remains at least at the current level. _Note:_ Manually setting the `currentTime` will eventually fire a `canplaythrough` event in firefox. Other browsers might not fire this event. |
| `stalled` | Sent when the user agent is trying to fetch media data, but data is unexpectedly not forthcoming. |
@ -515,11 +576,9 @@ player.on('ready', event => {
#### YouTube only
| Event Type | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `statechange` | The state of the player has changed. The code can be accessed via `event.detail.code`. Possible values are `-1`: Unstarted, `0`: Ended, `1`: Playing, `2`: Paused, `3`: Buffering, `5`: Video cued. See the [YouTube Docs](https://developers.google.com/youtube/iframe_api_reference#onStateChange) for more information. |
| `qualitychange` | The quality of playback has changed. |
| `qualityrequested` | A change to playback quality has been requested. _Note:_ A change to quality can only be _requested_ via the API. There is no guarantee the quality will change to the level requested. You should listen to the `qualitychange` event for true changes. |
| Event Type | Description |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `statechange` | The state of the player has changed. The code can be accessed via `event.detail.code`. Possible values are `-1`: Unstarted, `0`: Ended, `1`: Playing, `2`: Paused, `3`: Buffering, `5`: Video cued. See the [YouTube Docs](https://developers.google.com/youtube/iframe_api_reference#onStateChange) for more information. |
_Note:_ These events also bubble up the DOM. The event target will be the container element.
@ -531,8 +590,8 @@ YouTube and Vimeo are currently supported and function much like a HTML5 video.
to access the API's directly. You can do so via the `embed` property of your player object - e.g. `player.embed`. You can then use the relevant methods from the
third party APIs. More info on the respective API's here:
* [YouTube iframe API Reference](https://developers.google.com/youtube/iframe_api_reference)
* [Vimeo player.js Reference](https://github.com/vimeo/player.js)
- [YouTube iframe API Reference](https://developers.google.com/youtube/iframe_api_reference)
- [Vimeo player.js Reference](https://github.com/vimeo/player.js)
_Note_: Not all API methods may work 100%. Your mileage may vary. It's better to use the Plyr API where possible.
@ -555,37 +614,32 @@ 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)
## Fullscreen
Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen).
## Browser support
Plyr supports the last 2 versions of most _modern_ browsers. IE11 is also supported.
Plyr supports the last 2 versions of most _modern_ browsers.
| Browser | Supported |
| ------------- | -------------- |
| Safari | |
| Mobile Safari | &sup1; |
| Firefox | |
| Chrome | |
| Opera | |
| Edge | |
| IE10+ | &sup2; |
| IE9 | API only&sup3; |
| Browser | Supported |
| ------------- | ------------- |
| Safari | |
| Mobile Safari | &sup1; |
| Firefox | |
| Chrome | |
| Opera | |
| Edge | |
| IE11 | &sup3; |
| IE10 | &sup2;&sup3; |
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled.
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported (v1.0.28+)
3. IE10 has no native fullscreen support, fallback can be used (see [options](#options))
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide.
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)).
3. Polyfills required. See below.
### Polyfills
Plyr uses ES6 which isn't supported in all browsers quite yet. This means some features will need to be polyfilled to be available otherwise you'll run into issues. We've elected to not burden the ~90% of users that do support these features with extra JS and instead leave polyfilling to you to work out based on your needs. The easiest method I've found is to use [polyfill.io](https://polyfill.io) which provides polyfills based on user agent. This is the method the demo uses.
### Checking for support
@ -597,9 +651,9 @@ const supported = Plyr.supported('video', 'html5', true);
The arguments are:
* Media type (`audio` or `video`)
* Provider (`html5`, `youtube` or `vimeo`)
* Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
- Media type (`audio` or `video`)
- Provider (`html5`, `youtube` or `vimeo`)
- Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
### Disable support programatically
@ -630,33 +684,35 @@ Plyr is developed by [@sam_potts](https://twitter.com/sam_potts) / [sampotts.me]
## Donate
Plyr costs money to run, not only my time - I donate that for free but domains, hosting and more. Any help is appreciated...
[Donate to support Plyr](https://www.paypal.me/pottsy/20usd)
Plyr costs money to run, not only my time. I donate my time for free as I enjoy building Plyr but unfortunately have to pay for domains, hosting, and more. Any help with costs is appreciated...
- [Donate via Patreon](https://www.patreon.com/plyr)
- [Donate via PayPal](https://www.paypal.me/pottsy/20usd)
## Mentions
* [ProductHunt](https://www.producthunt.com/tech/plyr)
* [The Changelog](http://thechangelog.com/plyr-simple-html5-media-player-custom-controls-webvtt-captions/)
* [HTML5 Weekly #177](http://html5weekly.com/issues/177)
* [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f)
* [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/)
* [Hacker News](https://news.ycombinator.com/item?id=9136774)
* [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04)
* [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player)
* [The Treehouse Show #131](https://teamtreehouse.com/library/episode-131-origami-react-responsive-hero-images)
* [noupe.com](http://www.noupe.com/design/html5-plyr-is-a-responsive-and-accessible-video-player-94389.html)
- [ProductHunt](https://www.producthunt.com/tech/plyr)
- [The Changelog](http://thechangelog.com/plyr-simple-html5-media-player-custom-controls-webvtt-captions/)
- [HTML5 Weekly #177](http://html5weekly.com/issues/177)
- [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f)
- [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/)
- [Hacker News](https://news.ycombinator.com/item?id=9136774)
- [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04)
- [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player)
- [The Treehouse Show #131](https://teamtreehouse.com/library/episode-131-origami-react-responsive-hero-images)
- [noupe.com](http://www.noupe.com/design/html5-plyr-is-a-responsive-and-accessible-video-player-94389.html)
## Used by
* [Selz.com](https://selz.com)
* [Peugeot.fr](http://www.peugeot.fr/marque-et-technologie/technologies/peugeot-i-cockpit.html)
* [Peugeot.de](http://www.peugeot.de/modelle/modellberater/208-3-turer/fotos-videos.html)
* [TomTom.com](http://prioritydriving.tomtom.com/)
* [DIGBMX](http://digbmx.com/)
* [Grime Archive](https://grimearchive.com/)
* [koel - A personal music streaming server that works.](http://koel.phanan.net/)
* [Oscar Radio](http://oscar-radio.xyz/)
* [Sparkk TV](https://www.sparkktv.com/)
- [Selz.com](https://selz.com)
- [Peugeot.fr](http://www.peugeot.fr/marque-et-technologie/technologies/peugeot-i-cockpit.html)
- [Peugeot.de](http://www.peugeot.de/modelle/modellberater/208-3-turer/fotos-videos.html)
- [TomTom.com](http://prioritydriving.tomtom.com/)
- [DIGBMX](http://digbmx.com/)
- [Grime Archive](https://grimearchive.com/)
- [koel - A personal music streaming server that works.](http://koel.phanan.net/)
- [Oscar Radio](http://oscar-radio.xyz/)
- [Sparkk TV](https://www.sparkktv.com/)
Let me know on [Twitter](https://twitter.com/sam_potts) I can add you to the above list. It'd be awesome to see how you're using Plyr :-)
@ -664,15 +720,19 @@ Let me know on [Twitter](https://twitter.com/sam_potts) I can add you to the abo
Credit to the PayPal HTML5 Video player from which Plyr's caption functionality was originally ported from:
* [PayPal's Accessible HTML5 Video Player](https://github.com/paypal/accessible-html5-video-player)
* [An awesome guide for Plyr in Japanese!](http://syncer.jp/how-to-use-plyr-io) by [@arayutw](https://twitter.com/arayutw)
- [PayPal's Accessible HTML5 Video Player](https://github.com/paypal/accessible-html5-video-player)
- [An awesome guide for Plyr in Japanese!](http://syncer.jp/how-to-use-plyr-io) by [@arayutw](https://twitter.com/arayutw)
## Thanks
[![Fastly](https://www.fastly.com/sites/all/themes/custom/fastly2016/logo.png)](https://www.fastly.com/)
[![Fastly](https://cdn.plyr.io/static/fastly-logo.png)](https://www.fastly.com/)
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.
[![Sentry](https://cdn.plyr.io/static/sentry-logo-black.svg)](https://sentry.io/)
Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site.
## Copyright and License
[The MIT license](license.md)

View File

@ -1,10 +1,26 @@
// ==========================================================================
// Plyr Captions
// TODO: Create as class
// ==========================================================================
import support from './support';
import utils from './utils';
import controls from './controls';
import support from './support';
import { dedupe } from './utils/arrays';
import browser from './utils/browser';
import {
createElement,
emptyElement,
getAttributesFromSelector,
insertAfter,
removeElement,
toggleClass,
} from './utils/elements';
import { on, triggerEvent } from './utils/events';
import fetch from './utils/fetch';
import i18n from './utils/i18n';
import is from './utils/is';
import { getHTML } from './utils/strings';
import { parseUrl } from './utils/urls';
const captions = {
// Setup captions
@ -14,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 (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);
}
@ -47,151 +45,339 @@ 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)));
// Fix IE captions if CORS is used
// Fetch captions and inject as blobs instead (data URIs not supported!)
if (browser.isIE && window.URL) {
const elements = this.media.querySelectorAll('track');
// If no caption file exists, hide container for caption text
if (utils.is.empty(captions.getTracks.call(this))) {
return;
Array.from(elements).forEach(track => {
const src = track.getAttribute('src');
const url = parseUrl(src);
if (
url !== null &&
url.hostname !== window.location.href.hostname &&
['http:', 'https:'].includes(url.protocol)
) {
fetch(src, 'blob')
.then(blob => {
track.setAttribute('src', window.URL.createObjectURL(blob));
})
.catch(() => {
removeElement(track);
});
}
});
}
// 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 browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));
// Set available languages in list
if (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 => {
// Remove previous bindings
utils.on(track, 'cuechange', event => captions.setCue.call(this, event));
// Turn off native caption rendering to avoid double captions
// eslint-disable-next-line
track.mode = 'hidden';
});
// Get current track
const currentTrack = captions.getCurrentTrack.call(this);
// 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);
}
}
} else if (this.isVimeo && this.captions.active) {
this.embed.enableTextTrack(this.language);
}
},
// 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() {
return captions.getTracks.call(this).find(track => track.language.toLowerCase() === this.language);
},
// 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 active = track.activeCues[0];
const currentTrack = captions.getCurrentTrack.call(this);
// Only display current track
if (track !== currentTrack) {
// 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;
}
// Display a cue, if there is one
if (utils.is.cue(active)) {
captions.setText.call(this, active.getCueAsHTML());
} else {
captions.setText.call(this, null);
}
const { toggled } = this.captions; // Current state
const activeClass = this.config.classNames.captions.active;
utils.dispatchEvent.call(this, this.media, 'cuechange');
// Get the next state
// If the method is called without parameter, toggle based on current value
const active = is.nullOrUndefined(input) ? !toggled : input;
// Update state and trigger event
if (active !== toggled) {
// When passive, don't override user preferences
if (!passive) {
this.captions.active = active;
this.storage.set({ captions: active });
}
// Force language if the call isn't passive and there is no matching language to toggle to
if (!this.language && active && !passive) {
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);
// Override user preferences to avoid switching languages if a matching track is added
this.captions.language = track.language;
// Set caption, but don't store in localStorage as user preference
captions.set.call(this, tracks.indexOf(track));
return;
}
// Toggle button if it's enabled
if (this.elements.buttons.captions) {
this.elements.buttons.captions.pressed = active;
}
// Add class hook
toggleClass(this.elements.container, activeClass, active);
this.captions.toggled = active;
// Update settings menu
controls.updateSetting.call(this, 'captions');
// Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
}
},
// Set the current caption
setText(input) {
// Set captions by track index
// Used internally for the currentTrack setter with the passive option forced to false
set(index, passive = true) {
const tracks = captions.getTracks.call(this);
// Disable captions if setting to -1
if (index === -1) {
captions.toggle.call(this, false, passive);
return;
}
if (!is.number(index)) {
this.debug.warn('Invalid caption argument', index);
return;
}
if (!(index in tracks)) {
this.debug.warn('Track not found', index);
return;
}
if (this.captions.currentTrack !== index) {
this.captions.currentTrack = index;
const track = tracks[index];
const { language } = track || {};
// Store reference to node for invalidation on remove
this.captions.currentTrackNode = track;
// Update settings menu
controls.updateSetting.call(this, 'captions');
// When passive, don't override user preferences
if (!passive) {
this.captions.language = language;
this.storage.set({ language });
}
// 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 (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
currentTrack = captions.getCurrentTrack.call(this);
}
if (is.track(currentTrack)) {
if (!is.empty(currentTrack.label)) {
return currentTrack.label;
}
if (!is.empty(currentTrack.language)) {
return track.language.toUpperCase();
}
return i18n.get('enabled', this.config);
}
return i18n.get('disabled', this.config);
},
// 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() {
// If there's no caption toggle, bail
if (!utils.is.element(this.elements.buttons.captions)) {
return;
}
// 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;
// Only accept array or empty input
if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
this.debug.warn('updateCues: Invalid input', input);
return;
}
if (active) {
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, true);
utils.toggleState(this.elements.buttons.captions, true);
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');
}
},
};

View File

@ -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,
@ -47,8 +51,8 @@ const defaults = {
// Auto hide the controls
hideControls: true,
// Revert to poster on finish (HTML5 - will cause reload)
showPosterOnEnd: false,
// Reset to start when playback ended
resetOnEnd: false,
// Disable the standard context menu
disableContextMenu: true,
@ -56,25 +60,15 @@ const defaults = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/2.0.10/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',
// Quality default
quality: {
default: 'default',
options: [
'hd2160',
'hd1440',
'hd1080',
'hd720',
'large',
'medium',
'small',
'tiny',
'default',
],
default: 576,
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
},
// Set loops
@ -87,15 +81,7 @@ const defaults = {
// Speed default and options to display
speed: {
selected: 1,
options: [
0.5,
0.75,
1,
1.25,
1.5,
1.75,
2,
],
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
},
// Keyboard shortcut settings
@ -113,13 +99,17 @@ const defaults = {
// Captions settings
captions: {
active: false,
language: window.navigator.language.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
fullscreen: {
enabled: true, // Allow fullscreen?
fallback: true, // Fallback for vintage browsers
iosNative: false, // Use the native fullscreen in iOS (disables custom controls)
},
// Local storage
@ -131,7 +121,10 @@ const defaults = {
// Default controls
controls: [
'play-large',
// 'restart',
// 'rewind',
'play',
// 'fast-forward',
'progress',
'current-time',
'mute',
@ -142,20 +135,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',
forward: 'Forward {seektime} secs',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
@ -170,27 +160,42 @@ const defaults = {
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
loop: 'Loop',
start: 'Start',
end: 'End',
all: 'All',
reset: 'Reset',
none: 'None',
disabled: 'Disabled',
enabled: 'Enabled',
advertisement: 'Ad',
qualityBadge: {
2160: '4K',
1440: 'HD',
1080: 'HD',
720: 'HD',
576: 'SD',
480: 'SD',
},
},
// URLs
urls: {
vimeo: {
api: 'https://player.vimeo.com/api/player.js',
sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}',
api: 'https://vimeo.com/api/v2/video/{0}.json',
},
youtube: {
api: 'https://www.youtube.com/iframe_api',
sdk: 'https://www.youtube.com/iframe_api',
api:
'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
},
googleIMA: {
api: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
},
},
@ -201,7 +206,7 @@ const defaults = {
pause: null,
restart: null,
rewind: null,
forward: null,
fastForward: null,
mute: null,
volume: null,
captions: null,
@ -251,13 +256,14 @@ const defaults = {
// YouTube
'statechange',
// Quality
'qualitychange',
'qualityrequested',
// Ads
'adsloaded',
'adscontentpause',
'adsconentresume',
'adscontentresume',
'adstarted',
'adsmidpoint',
'adscomplete',
@ -281,7 +287,7 @@ const defaults = {
pause: '[data-plyr="pause"]',
restart: '[data-plyr="restart"]',
rewind: '[data-plyr="rewind"]',
forward: '[data-plyr="fast-forward"]',
fastForward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]',
fullscreen: '[data-plyr="fullscreen"]',
@ -300,13 +306,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',
},
@ -314,16 +320,20 @@ const defaults = {
// Class hooks added to the player in different states
classNames: {
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
ads: 'plyr__ads',
control: 'plyr__control',
type: 'plyr--{0}',
provider: 'plyr--{0}',
stopped: 'plyr--stopped',
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',
@ -333,6 +343,9 @@ const defaults = {
isTouch: 'plyr--is-touch',
uiSupported: 'plyr--full-ui',
noTransition: 'plyr--no-transition',
display: {
time: 'plyr__time',
},
menu: {
value: 'plyr__menu__value',
badge: 'plyr__badge',
@ -357,14 +370,24 @@ const defaults = {
tabFocus: 'plyr__tab-focus',
},
// Embed attributes
attributes: {
embed: {
provider: 'data-plyr-provider',
id: 'data-plyr-embed-id',
},
},
// API keys
keys: {
google: null,
},
// Ads
// Advertisements plugin
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: {
tag: null,
enabled: false,
publisherId: '',
},
};

View File

@ -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 };

View File

@ -17,10 +17,12 @@ export default class Console {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
}
get warn() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
}
get error() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;

1634
src/js/controls.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -1,127 +1,220 @@
// ==========================================================================
// Plyr fullscreen API
// Fullscreen wrapper
// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing
// ==========================================================================
import utils from './utils';
import browser from './utils/browser';
import { hasClass, toggleClass, trapFocus } from './utils/elements';
import { on, triggerEvent } from './utils/events';
import is from './utils/is';
// Determine the prefix
const prefix = (() => {
let value = false;
function onChange() {
if (!this.enabled) {
return;
}
if (utils.is.function(document.cancelFullScreen)) {
value = '';
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (is.element(button)) {
button.pressed = this.active;
}
// Trigger an event
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
// Trap focus in container
if (!browser.isIos) {
trapFocus.call(this.player, this.target, this.active);
}
}
function toggleFallback(toggle = false) {
// Store or restore scroll position
if (toggle) {
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
}
// Toggle scroll
document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Toggle button and fire events
onChange.call(this);
}
class Fullscreen {
constructor(player) {
// Keep reference to parent
this.player = player;
// Get prefix
this.prefix = Fullscreen.prefix;
this.property = Fullscreen.property;
// Scroll position
this.scrollPosition = { x: 0, y: 0 };
// Register event listeners
// Handle event (incase user presses escape etc)
on.call(
this.player,
document,
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => {
// TODO: Filter for target??
onChange.call(this);
},
);
// Fullscreen toggle on double click
on.call(this.player, this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return;
}
this.toggle();
});
// Update the UI
this.update();
}
// Determine if native supported
static get native() {
return !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled
);
}
// Get the prefix for handlers
static get prefix() {
// No prefix
if (is.function(document.exitFullscreen)) {
return '';
}
// Check for fullscreen support by vendor prefix
[
'webkit',
'o',
'moz',
'ms',
'khtml',
].some(pre => {
if (utils.is.function(document[`${pre}CancelFullScreen`])) {
let value = '';
const prefixes = ['webkit', 'moz', 'ms'];
prefixes.some(pre => {
if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
value = pre;
return true;
} else if (utils.is.function(document.msExitFullscreen) && document.msFullscreenEnabled) {
// Special case for MS (when isn't it?)
value = 'ms';
return true;
}
return false;
});
return value;
}
return value;
})();
static get property() {
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
}
// Fullscreen API
const fullscreen = {
// Get the prefix
prefix,
// Determine if fullscreen is enabled
get enabled() {
return (
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
this.player.config.fullscreen.enabled &&
this.player.supported.ui &&
this.player.isVideo
);
}
// Check if we can use it
enabled: document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled,
// Yet again Microsoft awesomeness,
// Sometimes the prefix is 'ms', sometimes 'MS' to keep you on your toes
eventType: prefix === 'ms' ? 'MSFullscreenChange' : `${prefix}fullscreenchange`,
// Is an element fullscreen
isFullScreen(element) {
if (!fullscreen.enabled) {
// Get active state
get active() {
if (!this.enabled) {
return false;
}
const target = utils.is.nullOrUndefined(element) ? document.body : element;
switch (prefix) {
case '':
return document.fullscreenElement === target;
case 'moz':
return document.mozFullScreenElement === target;
default:
return document[`${prefix}FullscreenElement`] === target;
// Fallback using classname
if (!Fullscreen.native) {
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
}
},
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
return element === this.target;
}
// Get target element
get target() {
return browser.isIos && this.player.config.fullscreen.iosNative
? this.player.media
: this.player.elements.container;
}
// Update UI
update() {
if (this.enabled) {
this.player.debug.log(`${Fullscreen.native ? 'Native' : 'Fallback'} fullscreen enabled`);
} else {
this.player.debug.log('Fullscreen not supported and fallback disabled');
}
// Add styling hook to show button
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
}
// Make an element fullscreen
requestFullScreen(element) {
if (!fullscreen.enabled) {
return false;
}
const target = utils.is.nullOrUndefined(element) ? document.body : element;
return !prefix.length ? target.requestFullScreen() : target[prefix + (prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen')]();
},
// Bail from fullscreen
cancelFullScreen() {
if (!fullscreen.enabled) {
return false;
}
return !prefix.length ? document.cancelFullScreen() : document[prefix + (prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen')]();
},
// Get the current element
element() {
if (!fullscreen.enabled) {
return null;
}
return !prefix.length ? document.fullscreenElement : document[`${prefix}FullscreenElement`];
},
// Setup fullscreen
setup() {
if (!this.supported.ui || this.isAudio || !this.config.fullscreen.enabled) {
enter() {
if (!this.enabled) {
return;
}
// Check for native support
const nativeSupport = fullscreen.enabled;
// iOS native fullscreen doesn't need the request step
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitEnterFullscreen();
} else if (!Fullscreen.native) {
toggleFallback.call(this, true);
} else if (!this.prefix) {
this.target.requestFullscreen();
} else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`]();
}
}
if (nativeSupport || (this.config.fullscreen.fallback && !utils.inFrame())) {
this.debug.log(`${nativeSupport ? 'Native' : 'Fallback'} fullscreen enabled`);
// Bail from fullscreen
exit() {
if (!this.enabled) {
return;
}
// Add styling hook to show button
utils.toggleClass(this.elements.container, this.config.classNames.fullscreen.enabled, true);
// iOS native fullscreen
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen();
this.player.play();
} else if (!Fullscreen.native) {
toggleFallback.call(this, false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.property}`]();
}
}
// Toggle state
toggle() {
if (!this.active) {
this.enter();
} else {
this.debug.log('Fullscreen not supported and fallback disabled');
this.exit();
}
}
}
// Toggle state
if (this.elements.buttons && this.elements.buttons.fullscreen) {
utils.toggleState(this.elements.buttons.fullscreen, false);
}
// Trap focus in container
utils.trapFocus.call(this);
},
};
export default fullscreen;
export default Fullscreen;

117
src/js/html5.js Normal file
View File

@ -0,0 +1,117 @@
// ==========================================================================
// Plyr HTML5 helpers
// ==========================================================================
import support from './support';
import { removeElement } from './utils/elements';
import { triggerEvent } from './utils/events';
const html5 = {
getSources() {
if (!this.isHTML5) {
return [];
}
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() {
// Get sizes from <source> elements
return html5.getSources
.call(this)
.map(source => Number(source.getAttribute('size')))
.filter(Boolean);
},
extend() {
if (!this.isHTML5) {
return;
}
const player = this;
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
// Get sources
const sources = html5.getSources.call(player);
const source = sources.find(source => source.getAttribute('src') === player.source);
// Return size, if match is found
return source && Number(source.getAttribute('size'));
},
set(input) {
// Get sources
const sources = html5.getSources.call(player);
// Get first match for requested size
const source = sources.find(source => Number(source.getAttribute('size')) === input);
// No matching source found
if (!source) {
return;
}
// Get current state
const { currentTime, paused, preload, readyState } = player.media;
// Set new source
player.media.src = source.getAttribute('src');
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
if (preload !== 'none' || readyState) {
// Restore time
player.once('loadedmetadata', () => {
player.currentTime = currentTime;
// Resume playing
if (!paused) {
player.play();
}
});
// Load new source
player.media.load();
}
// Trigger change event
triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: input,
});
// Save to storage
player.storage.set({ quality: input });
},
});
},
// Cancel current network requests
// See https://github.com/sampotts/plyr/issues/174
cancelRequests() {
if (!this.isHTML5) {
return;
}
// Remove child sources
removeElement(html5.getSources.call(this));
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
this.media.setAttribute('src', this.config.blankVideo);
// Load the new empty source
// This will cancel existing requests
// See https://github.com/sampotts/plyr/issues/174
this.media.load();
// Debugging
this.debug.log('Cancelled network requests');
},
};
export default html5;

File diff suppressed because it is too large Load Diff

View File

@ -2,14 +2,10 @@
// Plyr Media
// ==========================================================================
import support from './support';
import utils from './utils';
import youtube from './plugins/youtube';
import html5 from './html5';
import vimeo from './plugins/vimeo';
import ui from './ui';
// Sniff out the browser
const browser = utils.getBrowser();
import youtube from './plugins/youtube';
import { createElement, toggleClass, wrap } from './utils/elements';
const media = {
// Setup media
@ -21,86 +17,43 @@ 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);
}
if (this.supported.ui) {
// Check for picture-in-picture support
utils.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);
// If there's no autoplay attribute, assume the video is stopped and add state class
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.config.autoplay);
// Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, support.touch);
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 = 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) {
ui.setTitle.call(this);
if (this.isHTML5) {
html5.extend.call(this);
} else if (this.isYouTube) {
youtube.setup.call(this);
} else if (this.isVimeo) {
vimeo.setup.call(this);
}
},
// Cancel current network requests
// See https://github.com/sampotts/plyr/issues/174
cancelRequests() {
if (!this.isHTML5) {
return;
}
// Remove child sources
Array.from(this.media.querySelectorAll('source')).forEach(utils.removeElement);
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
this.media.setAttribute('src', this.config.blankVideo);
// Load the new empty source
// This will cancel existing requests
// See https://github.com/sampotts/plyr/issues/174
this.media.load();
// Debugging
this.debug.log('Cancelled network requests');
},
};
export default media;

View File

@ -6,7 +6,13 @@
/* global google */
import utils from '../utils';
import { createElement } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import i18n from '../utils/i18n';
import is from '../utils/is';
import loadScript from '../utils/loadScript';
import { formatTime } from '../utils/time';
import { buildUrlParams } from '../utils/urls';
class Ads {
/**
@ -16,29 +22,9 @@ class Ads {
*/
constructor(player) {
this.player = player;
this.publisherId = player.config.ads.publisherId;
this.playing = false;
this.initialized = false;
this.enabled = utils.is.url(player.config.ads.tag);
// Check if a tag URL is provided.
if (!this.enabled) {
return;
}
// Check if the Google IMA3 SDK is loaded
if (!utils.is.object(window.google)) {
utils.loadScript(player.config.urls.googleIMA.api, () => {
this.ready();
});
} else {
this.ready();
}
}
/**
* Get the ads instance ready.
*/
ready() {
this.elements = {
container: null,
displayContainer: null,
@ -50,32 +36,82 @@ class Ads {
this.safetyTimer = null;
this.countdownTimer = null;
// Set listeners on the Plyr instance
this.listeners();
// Setup a promise to resolve when the IMA manager is ready
this.managerPromise = new Promise((resolve, reject) => {
// The ad is loaded and ready
this.on('loaded', resolve);
// Ads failed
this.on('error', reject);
});
this.load();
}
get enabled() {
return (
this.player.isHTML5 && 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 (!is.object(window.google) || !is.object(window.google.ima)) {
loadScript(this.player.config.urls.googleIMA.sdk)
.then(() => {
this.ready();
})
.catch(() => {
// Script failed to load or is blocked
this.trigger('error', new Error('Google IMA SDK failed to load'));
});
} else {
this.ready();
}
}
}
/**
* Get the ads instance ready
*/
ready() {
// Start ticking our safety timer. If the whole advertisement
// thing doesn't resolve within our set time; we bail
this.startSafetyTimer(12000, 'ready()');
// Setup a simple promise to resolve if the IMA loader is ready
this.loaderPromise = new Promise(resolve => {
this.on('ADS_LOADER_LOADED', () => resolve());
});
// Setup a promise to resolve if the IMA manager is ready
this.managerPromise = new Promise(resolve => {
this.on('ADS_MANAGER_LOADED', () => resolve());
});
// Clear the safety timer
this.managerPromise.then(() => {
this.clearSafetyTimer('onAdsManagerLoaded()');
});
// Set listeners on the Plyr instance
this.listeners();
// Setup the IMA SDK
this.setupIMA();
}
// Build the default tag URL
get tagUrl() {
const params = {
AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
AV_CHANNELID: '5a0458dc28a06145e4519d21',
AV_URL: window.location.hostname,
cb: Date.now(),
AV_WIDTH: 640,
AV_HEIGHT: 480,
AV_CDIM2: this.publisherId,
};
const base = 'https://go.aniview.com/api/adserver6/vast/';
return `${base}?${buildUrlParams(params)}`;
}
/**
* In order for the SDK to display ads for our video, we need to tell it where to put them,
* so here we define our ad container. This div is set up to render on top of the video player.
@ -86,9 +122,8 @@ 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,
hidden: '',
});
this.player.elements.container.appendChild(this.elements.container);
@ -117,12 +152,16 @@ 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
const request = new google.ima.AdsRequest();
request.adTagUrl = this.player.config.ads.tag;
request.adTagUrl = this.tagUrl;
// Specify the linear and nonlinear slot sizes. This helps the SDK
// to select the correct creative if multiple are returned
@ -134,9 +173,10 @@ class Ads {
// We only overlay ads as we only support video.
request.forceNonLinearFullSlot = false;
this.loader.requestAds(request);
// Mute based on current state
request.setAdWillPlayMuted(!this.player.muted);
this.handleEventListeners('ADS_LOADER_LOADED');
this.loader.requestAds(request);
} catch (e) {
this.onAdError(e);
}
@ -148,24 +188,30 @@ class Ads {
*/
pollCountdown(start = false) {
if (!start) {
window.clearInterval(this.countdownTimer);
clearInterval(this.countdownTimer);
this.elements.container.removeAttribute('data-badge-text');
return;
}
const update = () => {
const time = utils.formatTime(this.manager.getRemainingTime());
this.elements.container.setAttribute('data-badge-text', time);
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);
};
this.countdownTimer = window.setInterval(update, 100);
this.countdownTimer = setInterval(update, 100);
}
/**
* This method is called whenever the ads are ready inside the AdDisplayContainer
* @param {Event} adsManagerLoadedEvent
*/
onAdsManagerLoaded(adsManagerLoadedEvent) {
onAdsManagerLoaded(event) {
// Load could occur after a source change (race condition)
if (!this.enabled) {
return;
}
// Get the ads manager
const settings = new google.ima.AdsRenderingSettings();
@ -175,31 +221,29 @@ class Ads {
// The SDK is polling currentTime on the contentPlayback. And needs a duration
// so it can determine when to start the mid- and post-roll
this.manager = adsManagerLoadedEvent.getAdsManager(this.player, settings);
this.manager = event.getAdsManager(this.player, settings);
// Get the cue points for any mid-rolls by filtering out the pre- and post-roll
this.cuePoints = this.manager.getCuePoints();
// Add advertisement cue's within the time line if available
this.cuePoints.forEach(cuePoint => {
if (cuePoint !== 0 && cuePoint !== -1) {
const seekElement = this.player.elements.progress;
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 (seekElement) {
const cuePercentage = 100 / this.player.duration * cuePoint;
const cue = utils.createElement('span', {
class: this.player.config.classNames.cues,
});
if (is.element(seekElement)) {
const cuePercentage = 100 / this.player.duration * cuePoint;
const cue = createElement('span', {
class: this.player.config.classNames.cues,
});
cue.style.left = `${cuePercentage.toString()}%`;
seekElement.appendChild(cue);
cue.style.left = `${cuePercentage.toString()}%`;
seekElement.appendChild(cue);
}
}
}
});
// Get skippable state
// TODO: Skip button
// this.manager.getAdSkippableState();
});
}
// Set volume to match player
this.manager.setVolume(this.player.volume);
@ -214,7 +258,7 @@ class Ads {
});
// Resolve our adsManager
this.handleEventListeners('ADS_MANAGER_LOADED');
this.trigger('loaded');
}
/**
@ -232,17 +276,18 @@ class Ads {
// Proxy event
const dispatchEvent = type => {
utils.dispatchEvent.call(this.player, this.player.media, `ads${type}`);
const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
triggerEvent.call(this.player, this.player.media, event);
};
switch (event.type) {
case google.ima.AdEvent.Type.LOADED:
// This is the first event sent for an ad - it is possible to determine whether the
// ad is a video ad or an overlay
this.handleEventListeners('LOADED');
this.trigger('loaded');
// Bubble event
dispatchEvent('loaded');
dispatchEvent(event.type);
// Start countdown
this.pollCountdown(true);
@ -260,10 +305,9 @@ class Ads {
case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
// All ads for the current videos are done. We can now request new advertisements
// in case the video is re-played
this.handleEventListeners('ALL_ADS_COMPLETED');
// Fire event
dispatchEvent('allcomplete');
dispatchEvent(event.type);
// TODO: Example for what happens when a next video in a playlist would be loaded.
// So here we load a new video when all ads are done.
@ -295,9 +339,8 @@ class Ads {
// This event indicates the ad has started - the video player can adjust the UI,
// for example display a pause button and remaining time. Fired when content should
// be paused. This usually happens right before an ad is about to cover the content
this.handleEventListeners('CONTENT_PAUSE_REQUESTED');
dispatchEvent('contentpause');
dispatchEvent(event.type);
this.pauseContent();
@ -308,9 +351,8 @@ class Ads {
// appropriate UI actions, such as removing the timer for remaining time detection.
// Fired when content should be resumed. This usually happens when an ad finishes
// or collapses
this.handleEventListeners('CONTENT_RESUME_REQUESTED');
dispatchEvent('contentresume');
dispatchEvent(event.type);
this.pollCountdown();
@ -319,23 +361,11 @@ class Ads {
break;
case google.ima.AdEvent.Type.STARTED:
dispatchEvent('started');
break;
case google.ima.AdEvent.Type.MIDPOINT:
dispatchEvent('midpoint');
break;
case google.ima.AdEvent.Type.COMPLETE:
dispatchEvent('complete');
break;
case google.ima.AdEvent.Type.IMPRESSION:
dispatchEvent('impression');
break;
case google.ima.AdEvent.Type.CLICK:
dispatchEvent('click');
dispatchEvent(event.type);
break;
default:
@ -349,7 +379,7 @@ class Ads {
*/
onAdError(event) {
this.cancel();
this.player.debug.log('Ads error', event);
this.player.debug.warn('Ads error', event);
}
/**
@ -374,6 +404,10 @@ class Ads {
this.player.on('seeked', () => {
const seekedTime = this.player.currentTime;
if (is.empty(this.cuePoints)) {
return;
}
this.cuePoints.forEach((cuePoint, index) => {
if (time < cuePoint && cuePoint < seekedTime) {
this.manager.discardAdBreak();
@ -385,7 +419,9 @@ class Ads {
// Listen to the resizing of the window. And resize ad accordingly
// TODO: eventually implement ResizeObserver
window.addEventListener('resize', () => {
this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
if (this.manager) {
this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
}
});
}
@ -396,39 +432,41 @@ class Ads {
const { container } = this.player.elements;
if (!this.managerPromise) {
return;
this.resumeContent();
}
// Play the requested advertisement whenever the adsManager is ready
this.managerPromise.then(() => {
// Initialize the container. Must be done via a user action on mobile devices
this.elements.displayContainer.initialize();
this.managerPromise
.then(() => {
// Initialize the container. Must be done via a user action on mobile devices
this.elements.displayContainer.initialize();
try {
if (!this.initialized) {
// Initialize the ads manager. Ad rules playlist will start at this time
this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
try {
if (!this.initialized) {
// Initialize the ads manager. Ad rules playlist will start at this time
this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
// Call play to start showing the ad. Single video and overlay ads will
// start at this time; the call will be ignored for ad rules
this.manager.start();
// Call play to start showing the ad. Single video and overlay ads will
// start at this time; the call will be ignored for ad rules
this.manager.start();
}
this.initialized = true;
} catch (adError) {
// An error may be thrown if there was a problem with the
// VAST response
this.onAdError(adError);
}
this.initialized = true;
} catch (adError) {
// An error may be thrown if there was a problem with the
// VAST response
this.onAdError(adError);
}
});
})
.catch(() => {});
}
/**
* Resume our video.
* Resume our video
*/
resumeContent() {
// Hide our ad container
utils.toggleHidden(this.elements.container, true);
// Hide the advertisement container
this.elements.container.style.zIndex = '';
// Ad is stopped
this.playing = false;
@ -443,8 +481,8 @@ class Ads {
* Pause our video
*/
pauseContent() {
// Show our ad container.
utils.toggleHidden(this.elements.container, false);
// Show the advertisement container
this.elements.container.style.zIndex = 3;
// Ad is playing.
this.playing = true;
@ -466,7 +504,7 @@ class Ads {
}
// Tell our instance that we're done for now
this.handleEventListeners('ERROR');
this.trigger('error');
// Re-create our adsManager
this.loadAds();
@ -477,30 +515,38 @@ class Ads {
*/
loadAds() {
// Tell our adsManager to go bye bye
this.managerPromise.then(() => {
// Destroy our adsManager
if (this.manager) {
this.manager.destroy();
}
this.managerPromise
.then(() => {
// Destroy our adsManager
if (this.manager) {
this.manager.destroy();
}
// Re-set our adsManager promises
this.managerPromise = new Promise(resolve => {
this.on('ADS_MANAGER_LOADED', () => resolve());
this.player.debug.log(this.manager);
});
// Re-set our adsManager promises
this.managerPromise = new Promise(resolve => {
this.on('loaded', resolve);
this.player.debug.log(this.manager);
});
// Now request some new advertisements
this.requestAds();
});
// Now request some new advertisements
this.requestAds();
})
.catch(() => {});
}
/**
* Handles callbacks after an ad event was invoked
* @param {string} event - Event type
*/
handleEventListeners(event) {
if (utils.is.function(this.events[event])) {
this.events[event].call(this);
trigger(event, ...args) {
const handlers = this.events[event];
if (is.array(handlers)) {
handlers.forEach(handler => {
if (is.function(handler)) {
handler.apply(this, args);
}
});
}
}
@ -511,7 +557,12 @@ class Ads {
* @return {Ads}
*/
on(event, callback) {
this.events[event] = callback;
if (!is.array(this.events[event])) {
this.events[event] = [];
}
this.events[event].push(callback);
return this;
}
@ -526,7 +577,7 @@ class Ads {
startSafetyTimer(time, from) {
this.player.debug.log(`Safety timer invoked from: ${from}`);
this.safetyTimer = window.setTimeout(() => {
this.safetyTimer = setTimeout(() => {
this.cancel();
this.clearSafetyTimer('startSafetyTimer()');
}, time);
@ -537,7 +588,7 @@ class Ads {
* @param {string} from
*/
clearSafetyTimer(from) {
if (!utils.is.nullOrUndefined(this.safetyTimer)) {
if (!is.nullOrUndefined(this.safetyTimer)) {
this.player.debug.log(`Safety timer cleared from: ${from}`);
clearTimeout(this.safetyTimer);

View File

@ -2,23 +2,66 @@
// Vimeo plugin
// ==========================================================================
import utils from './../utils';
import captions from './../captions';
import ui from './../ui';
import captions from '../captions';
import controls from '../controls';
import ui from '../ui';
import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import loadScript from '../utils/loadScript';
import { format, stripHTML } from '../utils/strings';
import { buildUrlParams } from '../utils/urls';
// Parse Vimeo ID from URL
function parseId(url) {
if (is.empty(url)) {
return null;
}
if (is.number(Number(url))) {
return url;
}
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
return url.match(regex) ? RegExp.$2 : url;
}
// Get aspect ratio for dimensions
function getAspectRatio(width, height) {
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
const ratio = getRatio(width, height);
return `${width / ratio}:${height / ratio}`;
}
// Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) {
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.api, () => {
vimeo.ready.call(this);
});
if (!is.object(window.Vimeo)) {
loadScript(this.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(this);
})
.catch(error => {
this.debug.warn('Vimeo API failed to load', error);
});
} else {
vimeo.ready.call(this);
}
@ -27,12 +70,16 @@ 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 height = 200;
const offset = (height - padding) / (height / 50);
const [x, y] = (is.string(input) ? input : this.config.ratio).split(':');
const padding = 100 / x * y;
this.elements.wrapper.style.paddingBottom = `${padding}%`;
this.media.style.transform = `translateY(-${offset}%)`;
if (this.supported.ui) {
const height = 240;
const offset = (height - padding) / (height / 50);
this.media.style.transform = `translateY(-${offset}%)`;
}
},
// API Ready
@ -43,54 +90,88 @@ const vimeo = {
const options = {
loop: player.config.loop.active,
autoplay: player.autoplay,
// muted: player.muted,
byline: false,
portrait: false,
title: false,
speed: true,
transparent: 0,
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
};
const params = utils.buildUrlParams(options);
const id = utils.parseVimeoId(player.media.getAttribute('src'));
const params = buildUrlParams(options);
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(player.config.attributes.embed.id);
}
const id = parseId(source);
// Build an iframe
const iframe = utils.createElement('iframe');
const src = `https://player.vimeo.com/video/${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
fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (is.empty(response)) {
return;
}
// Get the URL for thumbnail
const url = new URL(response[0].thumbnail_large);
// Get original image
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// 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;
// Disable native text track rendering
if (player.supported.ui) {
player.embed.disableTextTrack();
}
// 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 = () => {
player.embed.stop().then(() => {
player.media.paused = true;
player.currentTime = 0;
});
player.pause();
player.currentTime = 0;
};
// Seeking
@ -100,23 +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);
// 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
});
},
});
@ -127,10 +212,18 @@ const vimeo = {
return speed;
},
set(input) {
player.embed.setPlaybackRate(input).then(() => {
speed = input;
utils.dispatchEvent.call(player, player.media, 'ratechange');
});
player.embed
.setPlaybackRate(input)
.then(() => {
speed = input;
triggerEvent.call(player, player.media, 'ratechange');
})
.catch(error => {
// Hide menu item (and menu if empty)
if (error.name === 'Error') {
controls.setSpeedMenu.call(player, []);
}
});
},
});
@ -143,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');
});
},
});
@ -155,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');
});
},
});
@ -171,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;
@ -181,9 +274,15 @@ const vimeo = {
// Source
let currentSrc;
player.embed.getVideoUrl().then(value => {
currentSrc = value;
});
player.embed
.getVideoUrl()
.then(value => {
currentSrc = value;
})
.catch(error => {
this.debug.warn(error);
});
Object.defineProperty(player.media, 'currentSrc', {
get() {
return currentSrc;
@ -198,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);
});
@ -220,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
@ -235,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
@ -256,53 +355,56 @@ 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
// https://github.com/sampotts/plyr/issues/891
player.embed.getDuration().then(value => {
if (value !== player.media.duration) {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('seeked', () => {
player.media.seeking = false;
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
window.setTimeout(() => ui.build.call(player), 0);
setTimeout(() => ui.build.call(player), 0);
},
};

View File

@ -2,24 +2,52 @@
// YouTube plugin
// ==========================================================================
import utils from './../utils';
import controls from './../controls';
import ui from './../ui';
import ui from '../ui';
import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import loadImage from '../utils/loadImage';
import loadScript from '../utils/loadScript';
import { format, generateId } from '../utils/strings';
// 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;
}
// 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.api);
loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
// Setup callback for the API
// YouTube has it's own system of course...
@ -44,10 +72,10 @@ const youtube = {
// Try via undocumented API method first
// This method disappears now and then though...
// https://github.com/sampotts/plyr/issues/709
if (utils.is.function(this.embed.getVideoData)) {
if (is.function(this.embed.getVideoData)) {
const { title } = this.embed.getVideoData();
if (utils.is.empty(title)) {
if (is.empty(title)) {
this.config.title = title;
ui.setTitle.call(this);
return;
@ -56,13 +84,12 @@ const youtube = {
// Or via Google API
const key = this.config.keys.google;
if (utils.is.string(key) && !utils.is.empty(key)) {
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${key}&fields=items(snippet(title))&part=snippet`;
if (is.string(key) && !is.empty(key)) {
const url = format(this.config.urls.youtube.api, videoId, key);
fetch(url)
.then(response => (response.ok ? response.json() : null))
.then(result => {
if (result !== null && utils.is.object(result)) {
if (is.object(result)) {
this.config.title = result.items[0].snippet.title;
ui.setTitle.call(this);
}
@ -83,15 +110,44 @@ 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;
}
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}
// Replace the <iframe> with a <div> due to YouTube API issues
const videoId = utils.parseYouTubeId(player.media.getAttribute('src'));
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);
// 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
@ -99,6 +155,7 @@ const youtube = {
videoId,
playerVars: {
autoplay: player.config.autoplay ? 1 : 0, // Autoplay
hl: player.config.hl, // iframe interface language
controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported
rel: 0, // No related vids
showinfo: 0, // Hide info
@ -108,8 +165,8 @@ const youtube = {
playsinline: 1, // Allow iOS inline playback
// Tracking for stats
origin: window && window.location.hostname,
widget_referrer: window && window.location.href,
// origin: window ? `${window.location.protocol}//${window.location.host}` : null,
widget_referrer: window ? window.location.href : null,
// Captions are flaky on YouTube
cc_load_policy: player.captions.active ? 1 : 0,
@ -117,55 +174,23 @@ const youtube = {
},
events: {
onError(event) {
// If we've already fired an error, don't do it again
// YouTube fires onError twice
if (utils.is.object(player.media.error)) {
return;
// YouTube may fire onError twice, so only handle it once
if (!player.media.error) {
const code = event.data;
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
const message =
{
2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
101: 'The owner of the requested video does not allow it to be played in embedded players.',
150: 'The owner of the requested video does not allow it to be played in embedded players.',
}[code] || 'An unknown error occured';
player.media.error = { code, message };
triggerEvent.call(player, player.media, 'error');
}
const detail = {
code: event.data,
};
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
switch (event.data) {
case 2:
detail.message =
'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.';
break;
case 5:
detail.message =
'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.';
break;
case 100:
detail.message =
'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.';
break;
case 101:
case 150:
detail.message = 'The owner of the requested video does not allow it to be played in embedded players.';
break;
default:
detail.message = 'An unknown error occured';
break;
}
player.media.error = detail;
utils.dispatchEvent.call(player, player.media, 'error');
},
onPlaybackQualityChange(event) {
// Get the instance
const instance = event.target;
// Get current quality
player.media.quality = instance.getPlaybackQuality();
utils.dispatchEvent.call(player, player.media, 'qualitychange');
},
onPlaybackRateChange(event) {
// Get the instance
@ -174,9 +199,13 @@ const youtube = {
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
utils.dispatchEvent.call(player, player.media, 'ratechange');
triggerEvent.call(player, player.media, 'ratechange');
},
onReady(event) {
// Bail if onReady has already been called. See issue #1108
if (is.function(player.media.play)) {
return;
}
// Get the instance
const instance = event.target;
@ -185,18 +214,17 @@ const youtube = {
// Create a faux HTML5 API using the YouTube API
player.media.play = () => {
assurePlaybackState.call(player, true);
instance.playVideo();
player.media.paused = false;
};
player.media.pause = () => {
assurePlaybackState.call(player, false);
instance.pauseVideo();
player.media.paused = true;
};
player.media.stop = () => {
instance.stopVideo();
player.media.paused = true;
};
player.media.duration = instance.getDuration();
@ -209,11 +237,14 @@ const youtube = {
return Number(instance.getCurrentTime());
},
set(time) {
// Set seeking flag
player.media.seeking = true;
// If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
if (player.paused && !player.embed.hasPlayed) {
player.embed.mute();
}
// Trigger seeking
utils.dispatchEvent.call(player, player.media, 'seeking');
// Set seeking state and trigger event
player.media.seeking = true;
triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
@ -230,21 +261,6 @@ const youtube = {
},
});
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
return instance.getPlaybackQuality();
},
set(input) {
// Trigger request event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
quality: input,
});
instance.setPlaybackQuality(input);
},
});
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
@ -254,7 +270,7 @@ const youtube = {
set(input) {
volume = input;
instance.setVolume(volume * 100);
utils.dispatchEvent.call(player, player.media, 'volumechange');
triggerEvent.call(player, player.media, 'volumechange');
},
});
@ -265,10 +281,10 @@ const youtube = {
return muted;
},
set(input) {
const toggle = utils.is.boolean(input) ? input : muted;
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
utils.dispatchEvent.call(player, player.media, 'volumechange');
triggerEvent.call(player, player.media, 'volumechange');
},
});
@ -294,20 +310,20 @@ 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
window.clearInterval(player.timers.buffering);
clearInterval(player.timers.buffering);
// Setup buffering
player.timers.buffering = window.setInterval(() => {
player.timers.buffering = setInterval(() => {
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
// Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
utils.dispatchEvent.call(player, player.media, 'progress');
triggerEvent.call(player, player.media, 'progress');
}
// Set last buffer point
@ -315,22 +331,30 @@ const youtube = {
// Bail if we're at 100%
if (player.media.buffered === 1) {
window.clearInterval(player.timers.buffering);
clearInterval(player.timers.buffering);
// Trigger event
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
triggerEvent.call(player, player.media, 'canplaythrough');
}
}, 200);
// Rebuild UI
window.setTimeout(() => ui.build.call(player), 50);
setTimeout(() => ui.build.call(player), 50);
},
onStateChange(event) {
// Get the instance
const instance = event.target;
// Reset timer
window.clearInterval(player.timers.playing);
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
@ -340,8 +364,18 @@ const youtube = {
// 3 Buffering
// 5 Video cued
switch (event.data) {
case -1:
// Update scrubber
triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
triggerEvent.call(player, player.media, 'progress');
break;
case 0:
player.media.paused = true;
assurePlaybackState.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) {
@ -349,48 +383,42 @@ 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');
}
}
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 = window.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, 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;
@ -398,7 +426,7 @@ const youtube = {
break;
}
utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, {
triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},

File diff suppressed because it is too large Load Diff

13
src/js/plyr.polyfilled.js Normal file
View File

@ -0,0 +1,13 @@
// ==========================================================================
// Plyr Polyfilled Build
// plyr.js v3.4.3
// 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;

View File

@ -2,22 +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);
});
}
},
@ -25,62 +28,50 @@ 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;
}
// Cancel current network requests
media.cancelRequests.call(this);
html5.cancelRequests.call(this);
// Destroy instance and re-setup
this.destroy.call(
this,
() => {
// TODO: Reset menus here
// Reset quality options
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.inline);
// 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;
}
@ -92,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', '');
@ -101,7 +92,7 @@ const source = {
if (this.config.muted) {
this.media.setAttribute('muted', '');
}
if (this.config.inline) {
if (this.config.playsinline) {
this.media.setAttribute('playsinline', '');
}
}
@ -111,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
@ -136,6 +127,9 @@ const source = {
// Setup interface
ui.build.call(this);
}
// Update the fullscreen support
this.fullscreen.update();
},
true,
);

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) {
@ -12,17 +13,18 @@ class Storage {
// Check for actual support (see if we can use it)
static get supported() {
if (!('localStorage' in window)) {
return false;
}
const test = '___test';
// Try to use it (it might be disabled, e.g. user is in private mode)
// see: https://github.com/sampotts/plyr/issues/131
try {
if (!('localStorage' in window)) {
return false;
}
const test = '___test';
// Try to use it (it might be disabled, e.g. user is in private mode)
// see: https://github.com/sampotts/plyr/issues/131
window.localStorage.setItem(test, test);
window.localStorage.removeItem(test);
return true;
} catch (e) {
return false;
@ -30,15 +32,19 @@ class Storage {
}
get(key) {
if (!Storage.supported || !this.enabled) {
return null;
}
const store = window.localStorage.getItem(this.key);
if (!Storage.supported || 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) {
@ -48,7 +54,7 @@ class Storage {
}
// Can only store objectst
if (!utils.is.object(object)) {
if (!is.object(object)) {
return;
}
@ -56,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));

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 = {
@ -12,37 +24,10 @@ const support = {
// Check for support
// Basic functionality vs full UI
check(type, provider, inline) {
let api = false;
let ui = false;
const browser = utils.getBrowser();
const playsInline = browser.isIPhone && inline && support.inline;
switch (`${provider}:${type}`) {
case 'html5:video':
api = support.video;
ui = api && support.rangeInput && (!browser.isIPhone || playsInline);
break;
case 'html5:audio':
api = support.audio;
ui = api && support.rangeInput;
break;
case 'youtube:video':
api = true;
ui = support.rangeInput && (!browser.isIPhone || playsInline);
break;
case 'vimeo:video':
api = true;
ui = support.rangeInput && !browser.isIPhone;
break;
default:
api = support.audio && support.video;
ui = api && support.rangeInput;
}
check(type, provider, playsinline) {
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
const api = support[type] || provider !== 'html5';
const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
return {
api,
@ -52,93 +37,47 @@ 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/
inline: 'playsInline' in document.createElement('video'),
playsinline: 'playsInline' in document.createElement('video'),
// Check for mime type support against a player instance
// Credits: http://diveintohtml5.info/everything.html
// Related: http://www.leanbackplayer.com/test/h5mt.html
mime(type) {
const { media } = this;
try {
// Bail if no checking function
if (!this.isHTML5 || !utils.is.function(media.canPlayType)) {
return false;
}
// 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');
@ -147,11 +86,11 @@ const support = {
})(),
// Touch
// Remember a device can be moust + touch enabled
// NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
touch: 'ontouchstart' in document.documentElement,
// Detect transitions support
transitions: utils.transitionEndEvent !== false,
transitions: transitionEndEvent !== false,
// Reduced motion iOS & MacOS setting
// https://webkit.org/blog/7551/responsive-design-for-motion/

View File

@ -2,16 +2,20 @@
// Plyr UI
// ==========================================================================
import utils from './utils';
import captions from './captions';
import controls from './controls';
import fullscreen from './fullscreen';
import listeners from './listeners';
import support from './support';
import browser from './utils/browser';
import { getElement, toggleClass } from './utils/elements';
import { ready, triggerEvent } from './utils/events';
import i18n from './utils/i18n';
import is from './utils/is';
import loadImage from './utils/loadImage';
const ui = {
addStyleHook() {
utils.toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
utils.toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
},
// Toggle native HTML5 media controls
@ -26,19 +30,13 @@ const ui = {
// Setup the UI
build() {
// Re-attach media element listeners
// TODO: Use event bubbling
listeners.media.call(this);
// TODO: Use event bubbling?
this.listeners.media();
// Don't setup interface if no support
if (!this.supported.ui) {
this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
// Remove controls
utils.removeElement.call(this, 'controls');
// Remove large play
utils.removeElement.call(this, 'buttons.play');
// Restore native controls
ui.toggleNativeControls.call(this, true);
@ -47,27 +45,21 @@ 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);
// Re-attach control listeners
listeners.controls.call(this);
}
// If there's no controls, bail
if (!utils.is.element(this.elements.controls)) {
return;
this.listeners.controls();
}
// Remove native controls
ui.toggleNativeControls.call(this);
// Setup fullscreen
fullscreen.setup.call(this);
// Captions
captions.setup.call(this);
// Setup captions for HTML5
if (this.isHTML5) {
captions.setup.call(this);
}
// Reset volume
this.volume = null;
@ -81,267 +73,183 @@ const ui = {
// Reset loop state
this.loop = null;
// Reset quality options
this.options.quality = [];
// Reset quality setting
this.quality = null;
// Reset volume display
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
toggleClass(
this.elements.container,
this.config.classNames.pip.supported,
support.pip && this.isHTML5 && this.isVideo,
);
// Check for airplay support
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
// Ready for API calls
this.ready = true;
// Ready event at end of execution stack
utils.dispatchEvent.call(this, this.media, 'ready');
setTimeout(() => {
triggerEvent.call(this, this.media, 'ready');
}, 0);
// Set the title
ui.setTitle.call(this);
// Assure the poster image is set, if the property was added before the element was created
if (this.poster) {
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
setTitle() {
// Find the current text
let label = this.config.i18n.play;
let label = i18n.get('play', this.config);
// If there's a media title set, use that for the label
if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) {
if (is.string(this.config.title) && !is.empty(this.config.title)) {
label += `, ${this.config.title}`;
// Set container label
this.elements.container.setAttribute('aria-label', this.config.title);
}
// If there's a play button, set label
if (utils.is.nodeList(this.elements.buttons.play)) {
Array.from(this.elements.buttons.play).forEach(button => {
button.setAttribute('aria-label', label);
});
}
Array.from(this.elements.buttons.play || []).forEach(button => {
button.setAttribute('aria-label', label);
});
// Set iframe title
// https://github.com/sampotts/plyr/issues/124
if (this.isEmbed) {
const iframe = utils.getElement.call(this, 'iframe');
const iframe = getElement.call(this, 'iframe');
if (!utils.is.element(iframe)) {
if (!is.element(iframe)) {
return;
}
// Default to media type
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
const title = !is.empty(this.config.title) ? this.config.title : 'video';
const format = i18n.get('frameTitle', this.config);
iframe.setAttribute('title', this.config.i18n.frameTitle.replace('{title}', title));
iframe.setAttribute('title', format.replace('{title}', title));
}
},
// Check playing state
checkPlaying() {
// Class hooks
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused);
// Toggle poster
togglePoster(enable) {
toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
},
// Set aria state
if (utils.is.nodeList(this.elements.buttons.play)) {
Array.from(this.elements.buttons.play).forEach(button => utils.toggleState(button, this.playing));
// Set the poster image (async)
// Used internally for the poster setter, with the passive option forced to false
setPoster(poster, passive = true) {
// Don't override if call is passive
if (passive && this.poster) {
return Promise.reject(new Error('Poster already set'));
}
// Set property synchronously to respect the call order
this.media.setAttribute('poster', poster);
// Wait until ui is ready
return (
ready
.call(this)
// Load image
.then(() => loadImage(poster))
.catch(err => {
// Hide poster on error unless it's been set by another call
if (poster === this.poster) {
ui.togglePoster.call(this, false);
}
// Rethrow
throw err;
})
.then(() => {
// Prevent race conditions
if (poster !== this.poster) {
throw new Error('setPoster cancelled by later call to setPoster');
}
})
.then(() => {
Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`,
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
ui.togglePoster.call(this, true);
return poster;
})
);
},
// Check playing state
checkPlaying(event) {
// Class hooks
toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set state
Array.from(this.elements.buttons.play || []).forEach(target => {
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':
value = (() => {
const { buffered } = this.media;
if (buffered && buffered.length) {
// HTML5
return utils.getPercentage(buffered.end(0), this.duration);
} else if (utils.is.number(buffered)) {
// YouTube returns between 0 and 1
return buffered * 100;
}
return 0;
})();
ui.setProgress.call(this, this.elements.display.buffer, value);
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 displayHours = utils.getHours(this.duration) > 0;
// eslint-disable-next-line no-param-reassign
target.textContent = utils.formatTime(time, displayHours, 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);
},
};

View File

@ -1,790 +0,0 @@
// ==========================================================================
// Plyr utils
// ==========================================================================
import support from './support';
import { providers } from './types';
const utils = {
// Check variable types
is: {
plyr(input) {
return this.instanceof(input, 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),
};
},
// Load an external script
loadScript(url, callback) {
const current = document.querySelector(`script[src="${url}"]`);
// Check script is not already referenced, if so wait for load
if (current !== null) {
current.callbacks = current.callbacks || [];
current.callbacks.push(callback);
return;
}
// Build the element
const element = document.createElement('script');
// Callback queue
element.callbacks = element.callbacks || [];
element.callbacks.push(callback);
// Bind callback
if (utils.is.function(callback)) {
element.addEventListener(
'load',
event => {
element.callbacks.forEach(cb => cb.call(null, event));
element.callbacks = null;
},
false,
);
}
// Set the URL after binding callback
element.src = url;
// Inject
const first = document.getElementsByTagName('script')[0];
first.parentNode.insertBefore(element, first);
},
// 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;
function updateSprite(data) {
// Inject content
this.innerHTML = data;
// Inject the SVG to the body
document.body.insertBefore(this, document.body.childNodes[0]);
}
// Only load once
if (!hasId || !document.querySelectorAll(`#${id}`).length) {
// 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);
updateSprite.call(container, data.content);
return;
}
}
// Get the sprite
fetch(url)
.then(response => (response.ok ? response.text() : null))
.then(text => {
if (text === null) {
return;
}
if (support.storage) {
window.localStorage.setItem(
prefix + id,
JSON.stringify({
content: text,
}),
);
}
updateSprite.call(container, text);
})
.catch(() => {});
}
},
// Generate a random ID
generateId(prefix) {
return `${prefix}-${Math.floor(Math.random() * 10000)}`;
},
// Determine if we're in an iframe
inFrame() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
},
// 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 an element
removeElement(element) {
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
return null;
}
element.parentNode.removeChild(element);
return 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.keys(attributes).forEach(key => {
element.setAttribute(key, attributes[key]);
});
},
// 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 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);
},
// Toggle hidden attribute on an element
toggleHidden(element, toggle) {
if (!utils.is.element(element)) {
return;
}
if (toggle) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
},
// 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);
},
// Find the UI controls and store references in custom controls
// TODO: Allow settings menus with custom controls
findElements() {
try {
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
// Buttons
this.elements.buttons = {
play: utils.getElements.call(this, this.config.selectors.buttons.play),
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
forward: utils.getElement.call(this, this.config.selectors.buttons.forward),
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
};
// Progress
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
// Inputs
this.elements.inputs = {
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
};
// Display
this.elements.display = {
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
duration: utils.getElement.call(this, this.config.selectors.display.duration),
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
};
// Seek tooltip
if (utils.is.element(this.elements.progress)) {
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
}
return true;
} catch (error) {
// Log it
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
// Restore native video controls
this.toggleNativeControls(true);
return false;
}
},
// 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() {
const focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
const first = focusable[0];
const last = focusable[focusable.length - 1];
utils.on(
this.elements.container,
'keydown',
event => {
// Bail if not tab key or not fullscreen
if (event.key !== 'Tab' || event.keyCode !== 9 || !this.fullscreen.active) {
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();
}
},
false,
);
},
// Toggle event listener
toggleListener(elements, event, callback, toggle, passive, capture) {
// Bail if no elements
if (utils.is.nullOrUndefined(elements)) {
return;
}
// If a nodelist is passed, call itself on each node
if (utils.is.nodeList(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 capture boolean
let options = utils.is.boolean(capture) ? capture : false;
// If passive events listeners are supported
if (support.passiveListeners) {
options = {
// Whether the listener can be passive (i.e. default never prevented)
passive: utils.is.boolean(passive) ? passive : true,
// Whether the listener is a capturing listener or not
capture: utils.is.boolean(capture) ? capture : false,
};
}
// 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, capture) {
utils.toggleListener(element, events, callback, true, passive, capture);
},
// Unbind event handler
off(element, events, callback, passive, capture) {
utils.toggleListener(element, events, callback, false, passive, capture);
},
// Trigger event
dispatchEvent(element, type, bubbles, detail) {
// Bail if no element
if (!element || !type) {
return;
}
// Create and dispatch the event
const event = new CustomEvent(type, {
bubbles: utils.is.boolean(bubbles) ? bubbles : false,
detail: Object.assign({}, detail, {
plyr: this instanceof Plyr ? 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) {
// 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);
},
// 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)}`;
},
// 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);
},
// 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) {
window.setTimeout(() => {
utils.toggleHidden(element, true);
element.offsetHeight; // eslint-disable-line
utils.toggleHidden(element, false);
}, 0);
},
};
export default utils;

36
src/js/utils/animation.js Normal file
View File

@ -0,0 +1,36 @@
// ==========================================================================
// Animation utils
// ==========================================================================
import { toggleHidden } from './elements';
import is from './is';
export const transitionEndEvent = (() => {
const element = document.createElement('span');
const events = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend',
};
const type = Object.keys(events).find(
event => element.style[event] !== undefined,
);
return is.string(type) ? events[type] : false;
})();
// Force repaint of element
export function repaint(element) {
setTimeout(() => {
try {
toggleHidden(element, true);
element.offsetHeight; // eslint-disable-line
toggleHidden(element, false);
} catch (e) {
// Do nothing
}
}, 0);
}

23
src/js/utils/arrays.js Normal file
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
src/js/utils/browser.js Normal file
View File

@ -0,0 +1,13 @@
// ==========================================================================
// Browser sniffing
// Unfortunately, due to mixed support, UA sniffing is required
// ==========================================================================
const browser = {
isIE: /* @cc_on!@ */ false || !!document.documentMode,
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform),
};
export default browser;

302
src/js/utils/elements.js Normal file
View File

@ -0,0 +1,302 @@
// ==========================================================================
// Element utils
// ==========================================================================
import { toggleListener } from './events';
import is from './is';
// Wrap an element
export function wrap(elements, wrapper) {
// Convert `elements` to an array, if necessary.
const targets = elements.length ? elements : [elements];
// Loops backwards to prevent having to clone the wrapper on the
// first element (see `child` below).
Array.from(targets)
.reverse()
.forEach((element, index) => {
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
// Cache the current parent and sibling.
const parent = element.parentNode;
const sibling = element.nextSibling;
// Wrap the element (is automatically removed from its current
// parent).
child.appendChild(element);
// If the element had a sibling, insert the wrapper before
// the sibling to maintain the HTML structure; otherwise, just
// append it to the parent.
if (sibling) {
parent.insertBefore(child, sibling);
} else {
parent.appendChild(child);
}
});
}
// Set attributes
export function setAttributes(element, attributes) {
if (!is.element(element) || is.empty(attributes)) {
return;
}
// Assume null and undefined attributes should be left out,
// Setting them would otherwise convert them to "null" and "undefined"
Object.entries(attributes)
.filter(([, value]) => !is.nullOrUndefined(value))
.forEach(([key, value]) => element.setAttribute(key, value));
}
// Create a DocumentFragment
export function createElement(type, attributes, text) {
// Create a new <element>
const element = document.createElement(type);
// Set all passed attributes
if (is.object(attributes)) {
setAttributes(element, attributes);
}
// Add text node
if (is.string(text)) {
element.innerText = text;
}
// Return built element
return element;
}
// Inaert an element after another
export function insertAfter(element, target) {
if (!is.element(element) || !is.element(target)) {
return;
}
target.parentNode.insertBefore(element, target.nextSibling);
}
// Insert a DocumentFragment
export function insertElement(type, parent, attributes, text) {
if (!is.element(parent)) {
return;
}
parent.appendChild(createElement(type, attributes, text));
}
// Remove element(s)
export function removeElement(element) {
if (is.nodeList(element) || is.array(element)) {
Array.from(element).forEach(removeElement);
return;
}
if (!is.element(element) || !is.element(element.parentNode)) {
return;
}
element.parentNode.removeChild(element);
}
// Remove all child elements
export function emptyElement(element) {
if (!is.element(element)) {
return;
}
let { length } = element.childNodes;
while (length > 0) {
element.removeChild(element.lastChild);
length -= 1;
}
}
// Replace element
export function replaceElement(newChild, oldChild) {
if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) {
return null;
}
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
}
// Get an attribute object from a string selector
export function getAttributesFromSelector(sel, existingAttributes) {
// For example:
// '.test' to { class: 'test' }
// '#test' to { id: 'test' }
// '[data-test="test"]' to { 'data-test': 'test' }
if (!is.string(sel) || is.empty(sel)) {
return {};
}
const attributes = {};
const existing = existingAttributes;
sel.split(',').forEach(s => {
// Remove whitespace
const selector = s.trim();
const className = selector.replace('.', '');
const stripped = selector.replace(/[[\]]/g, '');
// Get the parts and value
const parts = stripped.split('=');
const key = parts[0];
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
// Get the first character
const start = selector.charAt(0);
switch (start) {
case '.':
// Add to existing classname
if (is.object(existing) && is.string(existing.class)) {
existing.class += ` ${className}`;
}
attributes.class = className;
break;
case '#':
// ID selector
attributes.id = selector.replace('#', '');
break;
case '[':
// Attribute selector
attributes[key] = value;
break;
default:
break;
}
});
return attributes;
}
// Toggle hidden
export function toggleHidden(element, hidden) {
if (!is.element(element)) {
return;
}
let hide = hidden;
if (!is.boolean(hide)) {
hide = !element.hidden;
}
if (hide) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
}
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
export function toggleClass(element, className, force) {
if (is.nodeList(element)) {
return Array.from(element).map(e => toggleClass(e, className, force));
}
if (is.element(element)) {
let method = 'toggle';
if (typeof force !== 'undefined') {
method = force ? 'add' : 'remove';
}
element.classList[method](className);
return element.classList.contains(className);
}
return false;
}
// Has class name
export function hasClass(element, className) {
return is.element(element) && element.classList.contains(className);
}
// Element matches selector
export function matches(element, selector) {
const prototype = { Element };
function match() {
return Array.from(document.querySelectorAll(selector)).includes(this);
}
const matches =
prototype.matches ||
prototype.webkitMatchesSelector ||
prototype.mozMatchesSelector ||
prototype.msMatchesSelector ||
match;
return matches.call(element, selector);
}
// Find all elements
export function getElements(selector) {
return this.elements.container.querySelectorAll(selector);
}
// Find a single element
export function getElement(selector) {
return this.elements.container.querySelector(selector);
}
// Trap focus inside container
export function trapFocus(element = null, toggle = false) {
if (!is.element(element)) {
return;
}
const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
const first = focusable[0];
const last = focusable[focusable.length - 1];
const trap = event => {
// Bail if not tab key or not fullscreen
if (event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element
const focused = document.activeElement;
if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used
first.focus();
event.preventDefault();
} else if (focused === first && event.shiftKey) {
// Move focus to last element that can be tabbed if Shift is used
last.focus();
event.preventDefault();
}
};
toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
}
// Set focus and tab focus class
export function setFocus(element = null, tabFocus = false) {
if (!is.element(element)) {
return;
}
// Set regular focus
element.focus();
// If we want to mimic keyboard focus via tab
if (tabFocus) {
toggleClass(element, this.config.classNames.tabFocus);
}
}

120
src/js/utils/events.js Normal file
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
src/js/utils/fetch.js Normal file
View File

@ -0,0 +1,42 @@
// ==========================================================================
// Fetch wrapper
// Using XHR to avoid issues with older browsers
// ==========================================================================
export default function fetch(url, responseType = 'text') {
return new Promise((resolve, reject) => {
try {
const request = new XMLHttpRequest();
// Check for CORS support
if (!('withCredentials' in request)) {
return;
}
request.addEventListener('load', () => {
if (responseType === 'text') {
try {
resolve(JSON.parse(request.responseText));
} catch (e) {
resolve(request.responseText);
}
} else {
resolve(request.response);
}
});
request.addEventListener('error', () => {
throw new Error(request.status);
});
request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send();
} catch (e) {
reject(e);
}
});
}

34
src/js/utils/i18n.js Normal file
View File

@ -0,0 +1,34 @@
// ==========================================================================
// Plyr internationalization
// ==========================================================================
import is from './is';
import { getDeep } from './objects';
import { replaceAll } from './strings';
const i18n = {
get(key = '', config = {}) {
if (is.empty(key) || is.empty(config)) {
return '';
}
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 = replaceAll(string, key, value);
});
return string;
},
};
export default i18n;

65
src/js/utils/is.js Normal file
View File

@ -0,0 +1,65 @@
// ==========================================================================
// Type checking utils
// ==========================================================================
const getConstructor = input => (input !== null && typeof input !== 'undefined' ? input.constructor : null);
const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);
const isNullOrUndefined = input => input === null || typeof input === 'undefined';
const isObject = input => getConstructor(input) === Object;
const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);
const isString = input => getConstructor(input) === String;
const isBoolean = input => getConstructor(input) === Boolean;
const isFunction = input => getConstructor(input) === Function;
const isArray = input => Array.isArray(input);
const isWeakMap = input => instanceOf(input, WeakMap);
const isNodeList = input => instanceOf(input, NodeList);
const isElement = input => instanceOf(input, Element);
const isTextNode = input => getConstructor(input) === Text;
const isEvent = input => instanceOf(input, Event);
const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);
const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));
const isEmpty = input =>
isNullOrUndefined(input) ||
((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||
(isObject(input) && !Object.keys(input).length);
const isUrl = input => {
// Accept a URL object
if (instanceOf(input, window.URL)) {
return true;
}
// Add the protocol if required
let string = input;
if (!input.startsWith('http://') || !input.startsWith('https://')) {
string = `http://${input}`;
}
try {
return !isEmpty(new URL(string).hostname);
} catch (e) {
return false;
}
};
export default {
nullOrUndefined: isNullOrUndefined,
object: isObject,
number: isNumber,
string: isString,
boolean: isBoolean,
function: isFunction,
array: isArray,
weakMap: isWeakMap,
nodeList: isNodeList,
element: isElement,
textNode: isTextNode,
event: isEvent,
keyboardEvent: isKeyboardEvent,
cue: isCue,
track: isTrack,
url: isUrl,
empty: isEmpty,
};

19
src/js/utils/loadImage.js Normal file
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 });
});
}

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,
});
});
}

View File

@ -0,0 +1,76 @@
// ==========================================================================
// Sprite loader
// ==========================================================================
import Storage from '../storage';
import fetch from './fetch';
import is from './is';
// Load an external SVG sprite
export default function loadSprite(url, id) {
if (!is.string(url)) {
return;
}
const prefix = 'cache';
const hasId = is.string(id);
let isCached = false;
const exists = () => document.getElementById(id) !== null;
const update = (container, data) => {
container.innerHTML = data;
// Check again incase of race condition
if (hasId && exists()) {
return;
}
// Inject the SVG to the body
document.body.insertAdjacentElement('afterbegin', container);
};
// Only load once if ID set
if (!hasId || !exists()) {
const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
container.setAttribute('hidden', '');
if (hasId) {
container.setAttribute('id', id);
}
// Check in cache
if (useStorage) {
const cached = window.localStorage.getItem(`${prefix}-${id}`);
isCached = cached !== null;
if (isCached) {
const data = JSON.parse(cached);
update(container, data.content);
}
}
// Get the sprite
fetch(url)
.then(result => {
if (is.empty(result)) {
return;
}
if (useStorage) {
window.localStorage.setItem(
`${prefix}-${id}`,
JSON.stringify({
content: result,
}),
);
}
update(container, result);
})
.catch(() => {});
}
}

42
src/js/utils/objects.js Normal file
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
src/js/utils/strings.js Normal file
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
src/js/utils/time.js Normal file
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
src/js/utils/urls.js Normal file
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;
}

View File

@ -8,6 +8,7 @@
direction: ltr;
font-family: $plyr-font-family;
font-variant-numeric: tabular-nums; // Force monosace-esque number widths
font-weight: $plyr-font-weight-regular;
line-height: $plyr-line-height;
max-width: 100%;
@ -25,6 +26,12 @@
width: 100%;
}
button {
font: inherit;
line-height: inherit;
width: auto;
}
// Ignore focus
&:focus {
outline: 0;

View File

@ -3,9 +3,9 @@
// --------------------------------------------------------------
.plyr__badge {
background: $plyr-menu-color;
background: $plyr-badge-bg;
border-radius: 2px;
color: $plyr-menu-bg;
color: $plyr-badge-color;
font-size: $plyr-font-size-badge;
line-height: 1;
padding: 3px 4px;

View File

@ -17,11 +17,10 @@
padding: $plyr-control-spacing;
position: absolute;
text-align: center;
transform: translateY(-($plyr-control-spacing * 4));
transition: transform 0.4s ease-in-out;
width: 100%;
span {
.plyr__caption {
background: $plyr-captions-bg;
border-radius: 2px;
box-decoration-break: clone;
@ -53,6 +52,8 @@
display: block;
}
.plyr--hide-controls .plyr__captions {
transform: translateY(-($plyr-control-spacing * 1.5));
// If the lower controls are shown and not empty
.plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty) ~ .plyr__captions {
transform: translateY(-($plyr-control-spacing * 4));
}

View File

@ -34,14 +34,14 @@
}
// Change icons on state change
.plyr__control[aria-pressed='false'] .icon--pressed,
.plyr__control[aria-pressed='true'] .icon--not-pressed,
.plyr__control[aria-pressed='false'] .label--pressed,
.plyr__control[aria-pressed='true'] .label--not-pressed {
.plyr__control:not(.plyr__control--pressed) .icon--pressed,
.plyr__control.plyr__control--pressed .icon--not-pressed,
.plyr__control:not(.plyr__control--pressed) .label--pressed,
.plyr__control.plyr__control--pressed .label--not-pressed {
display: none;
}
// Audio styles
// Audio control
.plyr--audio .plyr__control {
&.plyr__tab-focus,
&:hover,
@ -51,6 +51,21 @@
}
}
// Video control
.plyr--video .plyr__control {
svg {
filter: drop-shadow(0 1px 1px rgba(#000, 0.15));
}
// Hover and tab focus
&.plyr__tab-focus,
&:hover,
&[aria-expanded='true'] {
background: $plyr-video-control-bg-hover;
color: $plyr-video-control-color-hover;
}
}
// Large play button (video only)
.plyr__control--overlaid {
background: rgba($plyr-video-control-bg-hover, 0.8);

View File

@ -11,67 +11,44 @@
.plyr__controls {
align-items: center;
display: flex;
justify-content: flex-end;
text-align: center;
// Spacing
> .plyr__control,
.plyr__progress,
.plyr__time,
.plyr__menu {
margin-left: ($plyr-control-spacing / 2);
&:first-child,
&:first-child + [data-plyr='pause'] {
margin-left: 0;
}
}
.plyr__menu,
.plyr__volume {
margin-left: ($plyr-control-spacing / 2);
}
.plyr__menu + .plyr__control,
> .plyr__control + .plyr__menu,
> .plyr__control + .plyr__control,
.plyr__progress + .plyr__control {
margin-left: floor($plyr-control-spacing / 4);
}
> .plyr__control:first-child,
> .plyr__control:first-child + [data-plyr='pause'] {
margin-left: 0;
margin-right: auto;
}
// Hide empty controls
&:empty {
display: none;
}
@media (min-width: $plyr-bp-sm) {
> .plyr__control,
.plyr__menu,
.plyr__progress,
.plyr__time,
.plyr__menu {
.plyr__volume {
margin-left: $plyr-control-spacing;
}
> .plyr__control + .plyr__control,
.plyr__menu + .plyr__control,
> .plyr__control + .plyr__menu {
margin-left: ($plyr-control-spacing / 2);
}
}
}
// Video controls
.plyr--video .plyr__controls {
background: linear-gradient(rgba($plyr-video-controls-bg, 0), rgba($plyr-video-controls-bg, 0.7));
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;
bottom: 0;
color: $plyr-video-control-color;
left: 0;
padding: ($plyr-control-spacing * 3.5) $plyr-control-spacing $plyr-control-spacing;
position: absolute;
right: 0;
transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
z-index: 2;
.plyr__control {
svg {
filter: drop-shadow(0 1px 1px rgba(#000, 0.15));
}
// Hover and tab focus
&.plyr__tab-focus,
&:hover,
&[aria-expanded='true'] {
background: $plyr-video-control-bg-hover;
color: $plyr-video-control-color-hover;
}
}
}
@ -83,7 +60,29 @@
padding: $plyr-control-spacing;
}
// Hide controls
// Video controls
.plyr--video .plyr__controls {
background: linear-gradient(
rgba($plyr-video-controls-bg, 0),
rgba($plyr-video-controls-bg, 0.7)
);
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;
bottom: 0;
color: $plyr-video-control-color;
left: 0;
padding: ($plyr-control-spacing * 2) ($plyr-control-spacing / 2) ($plyr-control-spacing / 2);
position: absolute;
right: 0;
transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
z-index: 3;
@media (min-width: $plyr-bp-sm) {
padding: ($plyr-control-spacing * 3.5) $plyr-control-spacing $plyr-control-spacing;
}
}
// Hide video controls
.plyr--video.plyr--hide-controls .plyr__controls {
opacity: 0;
pointer-events: none;

View File

@ -3,14 +3,12 @@
// YouTube, Vimeo, etc
// --------------------------------------------------------------
.plyr__video-embed {
// Default to 16:9 ratio but this is set by JavaScript based on config
$padding: ((100 / 16) * 9);
$height: 200;
$offset: to-percentage(($height - $padding) / ($height / 50));
// Default to 16:9 ratio but this is set by JavaScript based on config
$embed-padding: ((100 / 16) * 9);
.plyr__video-embed {
height: 0;
padding-bottom: to-percentage($padding);
padding-bottom: to-percentage($embed-padding);
position: relative;
iframe {
@ -22,15 +20,17 @@
user-select: none;
width: 100%;
}
}
// Vimeo hack
> div {
// If the full custom UI is supported
.plyr--full-ui .plyr__video-embed {
$height: 240;
$offset: to-percentage(($height - $embed-padding) / ($height / 50));
// Only used for Vimeo
> .plyr__video-embed__container {
padding-bottom: to-percentage($height);
position: relative;
transform: translateY(-$offset);
}
}
// To allow mouse events to be captured if full support
.plyr--full-ui .plyr__video-embed iframe {
pointer-events: none;
}

View File

@ -35,11 +35,12 @@
right: -3px;
text-align: left;
white-space: nowrap;
z-index: 1;
z-index: 3;
> div {
overflow: hidden;
transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1), width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1),
width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
// Arrow
@ -54,29 +55,42 @@
width: 0;
}
ul {
list-style: none;
margin: 0;
overflow: hidden;
[role='menu'] {
padding: $plyr-control-padding;
}
[role='menuitem'],
[role='menuitemradio'] {
margin-top: 2px;
&:first-child {
margin-top: 0;
}
}
// Options
.plyr__control {
align-items: center;
color: $plyr-menu-color;
display: flex;
padding: ceil($plyr-control-padding / 2) ($plyr-control-padding * 2);
font-size: $plyr-font-size-menu;
padding: ceil($plyr-control-padding / 2)
ceil($plyr-control-padding * 1.5);
user-select: none;
width: 100%;
> span {
align-items: inherit;
display: flex;
width: 100%;
}
&::after {
border: 4px solid transparent;
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
transition: border-color 0.2s ease;
}
&--forward {
@ -100,7 +114,6 @@
margin-bottom: floor($plyr-control-padding / 2);
padding-left: ceil($plyr-control-padding * 4);
position: relative;
width: calc(100% - #{$horizontal-padding});
&::after {
@ -128,50 +141,49 @@
}
}
label.plyr__control {
.plyr__control[role='menuitemradio'] {
padding-left: $plyr-control-padding;
input[type='radio'] + span {
background: rgba(#000, 0.1);
&::before,
&::after {
border-radius: 100%;
}
&::before {
background: rgba(#000, 0.1);
content: '';
display: block;
flex-shrink: 0;
height: 16px;
margin-right: $plyr-control-spacing;
position: relative;
transition: all 0.3s ease;
width: 16px;
&::after {
background: #fff;
border-radius: 100%;
content: '';
height: 6px;
left: 5px;
opacity: 0;
position: absolute;
top: 5px;
transform: scale(0);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 6px;
}
}
input[type='radio']:checked + span {
background: $plyr-color-main;
&::after {
background: #fff;
border: 0;
height: 6px;
left: 12px;
opacity: 0;
top: 50%;
transform: translateY(-50%) scale(0);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 6px;
}
&[aria-checked='true'] {
&::before {
background: $plyr-color-main;
}
&::after {
opacity: 1;
transform: scale(1);
transform: translateY(-50%) scale(1);
}
}
input[type='radio']:focus + span {
@include plyr-tab-focus();
}
&.plyr__tab-focus input[type='radio'] + span,
&:hover input[type='radio'] + span {
&.plyr__tab-focus::before,
&:hover::before {
background: rgba(#000, 0.1);
}
}
@ -181,7 +193,7 @@
align-items: center;
display: flex;
margin-left: auto;
margin-right: -$plyr-control-padding;
margin-right: -($plyr-control-padding - 2);
overflow: hidden;
padding-left: ceil($plyr-control-padding * 3.5);
pointer-events: none;

View File

@ -0,0 +1,22 @@
// --------------------------------------------------------------
// Faux poster overlay
// --------------------------------------------------------------
.plyr__poster {
background-color: #000;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: contain;
height: 100%;
left: 0;
opacity: 0;
position: absolute;
top: 0;
transition: opacity 0.2s ease;
width: 100%;
z-index: 1;
}
.plyr--stopped.plyr__poster-enabled .plyr__poster {
opacity: 1;
}

View File

@ -3,10 +3,19 @@
// --------------------------------------------------------------
.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 +28,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 +66,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 +93,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;
}

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