Compare commits

...

590 Commits

Author SHA1 Message Date
Sam Potts 747c5f5f31 chore(release): 3.7.7 2023-03-11 22:35:17 +11:00
Sam Potts 39c6049850 chore: minor syntax tweaks 2023-03-11 22:28:24 +11:00
Sam Potts 0202e8efb0 fix(a11y): leverage native :focus-visible in CSS 2023-03-11 21:15:32 +11:00
Sam Potts e17d0220c0 fix(a11y): add timer role to time elements 2023-03-11 21:14:43 +11:00
Sam Potts aa4eaec742 fix(a11y): don’t set tabindex on parent container 2023-03-11 21:14:13 +11:00
Sam Potts 851c39d44e chore(release): 3.7.6 2023-03-10 12:38:52 +11:00
Sam Potts f012a7df36 fix: revert postinstall script change 2023-03-10 12:36:06 +11:00
Sam Potts b8c40ead0a chore(release): 3.7.5 2023-03-10 07:29:25 +11:00
Sam Potts 874bb8d3e8 fix: replace pnpm with npm in scripts 2023-03-10 07:26:37 +11:00
Sam Potts 2b3e483b36 chore(release): 3.7.4 2023-03-09 23:14:21 +11:00
Sam Potts 69ef4ec827 chore: remove unused import 2023-03-09 23:11:39 +11:00
Sam Potts d6cc19f2d8 fix: remove erroneous scss 2023-03-09 23:09:01 +11:00
Sam Potts d67eea5942 docs: update screenshot 2023-03-09 23:02:18 +11:00
Sam Potts c4a566dbff feat: remove need for iOS-specific styling 2023-03-09 22:33:37 +11:00
Sam Potts 185bb4c7cc docs: add section on self hosting 2023-03-09 22:33:37 +11:00
Sam Potts d9161212ff chore(cspell): add dictionaries 2023-03-09 22:33:37 +11:00
Sam Potts ed0b901d11 chore(build): use pnpm instead of yarn 2023-03-09 22:33:37 +11:00
Sam Potts 62436d8e8e fix: fullscreen improvements for iOS & iPadOS 2023-03-09 22:33:21 +11:00
Sam Potts 5731245f4f Update README.md 2023-03-08 22:00:41 +11:00
Morgan Zolob 41e2454bd2 Call preview-thumbnails listeners() function on load (#2604)
The listeners() function in preview-thumbnails.js was never called, so some functionality was broken.

Fixes #2210.
2023-02-25 13:04:13 +11:00
dependabot[bot] 9248e322fc chore(deps): bump json5 from 1.0.1 to 1.0.2 (#2600)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-25 13:02:30 +11:00
André Melo fa634a1bbb fix(issue #2624): Changing player.embed function call from setVolume to setMuted in src/plugins/Vimeo.js. (#2628)
setVolume(0) used before by muted button, didn't work on IOS

Co-authored-by: Andre Fernandes Cristofolini Melo <andre.melo@dotgroup.com.br>
2023-02-25 13:02:06 +11:00
Roy Eden f7d2938f42 Fixed event key with space (#2581)
* Fixed event key with space

* Fixed spacebar `event.key` in `controls.js`
2022-12-31 11:15:13 +11:00
dependabot[bot] 7c88b58a1a chore(deps): bump decode-uri-component from 0.2.0 to 0.2.2 (#2583)
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-type: indirect
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-31 11:14:39 +11:00
Sam Potts 20bf5a8833 chore(release): 3.7.3 2022-11-17 17:36:00 +11:00
Sam Potts de0402bebf fix: simplify logic for isFunction assertion method 2022-11-17 12:23:41 +11:00
Sam Potts db1b89b1c1 chore: update types to include string for controls 2022-11-17 12:23:41 +11:00
Sam Potts ea3675fcdc fix: revert pip change for iphone and add comment 2022-11-17 12:23:41 +11:00
Sam Potts e8beabd6a8 chore: upgrade packages 2022-11-17 12:23:41 +11:00
Sam Potts 433acd6f41 chore: use .node-version instead of .nvmrc 2022-11-17 12:23:41 +11:00
Sam Potts 69ced1313a fix: force nowrap in progress tooltips 2022-11-17 12:23:41 +11:00
dependabot[bot] 511a7db7c2 chore(deps): bump minimatch from 3.0.4 to 3.1.2 (#2573)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-17 12:22:30 +11:00
ebraminio 6a62e3d085 i18n: Make captions autodetect text direction (#2540)
This uses browser mechanism for detecting appropiate direction for text, dir=auto.
2022-11-17 12:22:10 +11:00
Parveen Bhadoo aba00d0ab6 plyr.io used on Thousands of Index Websites (#2516) 2022-11-17 12:21:27 +11:00
Raad Altaie c115dfc6a6 fixed menu border radius bug (#2548) 2022-11-17 12:13:40 +11:00
happyjake 6030b300f6 fix iPhone no PIP bug (#2533) 2022-11-17 11:57:04 +11:00
dependabot[bot] fff26351c9 chore(deps): bump socket.io-parser from 4.0.4 to 4.0.5 (#2564)
Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 4.0.4 to 4.0.5.
- [Release notes](https://github.com/socketio/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-parser/compare/4.0.4...4.0.5)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-type: indirect
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-17 11:52:32 +11:00
Nikola Stamatović e0fb524382 navigator.platform is deprecated (#2530)
I'm submitting a little update for browser detection:

1. `navigator.platform` is deprecated - it's prefered to use `navigator.userAgent` https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform
2. No need for the brackets in the RegEx - you can test this on https://regex101.com

And that's about it. Maybe I missed the point, do let me know! ❤️
2022-09-15 15:08:03 +10:00
NoirHusky ebda039395 Added configurable property to elements for re-use (#2489) 2022-09-09 22:52:01 +10:00
dependabot[bot] ed456197f5 chore(deps): bump terser from 5.10.0 to 5.14.2 (#2506)
Bumps [terser](https://github.com/terser/terser) from 5.10.0 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-09 22:49:05 +10:00
Luis Velásquez 66187c348a docs: replace example video ID with one that still works (#2518) 2022-09-09 22:47:42 +10:00
Emil Karlsson cacaef7def Improve accessibility on control buttons with aria-pressed (#2523) 2022-09-09 22:46:24 +10:00
CK Hicks 42b8f7bdab Fix for calc() in newer Dart Sass versions (#2519) 2022-09-09 22:45:16 +10:00
Sam Potts c90f044dac v3.7.2
- Fix: Add `@babel/plugin-proposal-optional-chaining` to transform optional chaining in build output
2022-04-20 20:15:42 +10:00
Sam Potts 5f2cb90bc6 fix: add babel plugin to transform optional chaining 2022-04-20 20:12:08 +10:00
Sam Potts 537ad2fe7d v3.7.1
- Feat: Minor styling improvements to the preview thumbnails (🚨 Requires a SCSS/CSS update 🚨)
- Fix: fix invalid CSS @charset rule in Sass files (thanks @Hashen110!)
- Chore: Replace deprecated KeyboardEvent `keyCode` references to use `key` instead (thanks @Hashen110!)
- Various other code clean up and typo fixes (thanks @Hashen110!)
2022-04-20 15:02:17 +10:00
Sam Potts e7621bec0f chore: syntax tweak 2022-04-20 14:52:39 +10:00
Sam Potts e3acba5859 chore: add words to custom cspell dictionary 2022-04-20 14:50:59 +10:00
Sam Potts d7086074e8 chore: disable ads in demo site 2022-04-20 14:50:29 +10:00
Sam Potts 04e6c43da7 chore: fix typo in comment 2022-04-20 14:50:14 +10:00
Sam Potts 73bfb5211b feat: minor tweaks to the preview thumbs UI 2022-04-20 14:31:42 +10:00
Hashen 4a78b656da fix JSDoc comments (#2468) 2022-04-19 22:00:48 +10:00
Hashen 776fd099f3 fix invalid CSS @charset rule (#2466) 2022-04-19 22:00:20 +10:00
Hashen bdcd98f4a7 remove redundant local variable (#2467) 2022-04-19 21:59:29 +10:00
Nikolai 01417f958a Add website to showcase in README.md (#2339)
* Add website to showcase in README.md

* Move showcase entry to bottom of list
2022-04-19 00:59:27 +10:00
Sam Potts 412d5df06e fix: use consistent border-radius variable 2022-04-18 22:34:03 +10:00
Sam Potts 3e35461e04 chore: use key property of KeyboardEvent 2022-04-18 22:34:03 +10:00
Hashen 1edec28d86 remove deprecated keyCode (#2463) 2022-04-18 22:23:22 +10:00
Hashen 99a7e9e1d7 fix typo (#2464) 2022-04-18 22:16:06 +10:00
Hashen 24eef153cb remove deprecated keyCode (#2461) 2022-04-18 21:47:48 +10:00
Hashen 3f09cf566b fix typo in a comment (#2462)
* fix typo in a comment

* fix typo in a comment
2022-04-18 21:42:52 +10:00
Hashen 29318591c0 add markers types (#2460) 2022-04-18 21:24:35 +10:00
Sam Potts d434c9af16 v3.7.0
- Feat: Add markers support (🚨 Requires a SCSS/CSS update 🚨) (thanks @ForeverSc and @fengshuo!)
- Feat: Add support for MediaMetadata (thanks @Hashen110!)
- Fix: Pass this context to captions.setup (fixes #2352) (thanks @WilliamMHerring, @willherring and @zenyr!)
- Fix: Modify vimeo parseHash to use non-named capture groups (fixes #2396) (thanks @fekle!)
- Fix: Replace deprecated String.prototype.substr() (thanks @CommanderRoot!)
- Docs: Update speed option default to match the source (thanks @ozgurg!)
- Docs: SASS → Sass (thanks @toastal!)
2022-04-18 21:02:28 +10:00
Sam Potts 9ce1dd6808 docs: added more docs for markers 2022-04-18 20:47:10 +10:00
Sam Potts 28347a0d7e chore: remove travis config 2022-04-18 20:28:21 +10:00
Sam Potts 5f9d090c13 chore: configure spellcheck via cspell 2022-04-18 20:28:12 +10:00
Sam Potts 1d4869beee feat: tweaks to markers logic and design 2022-04-18 20:15:30 +10:00
Sam Potts 565b68a5e2 chore: package upgrades 2022-04-18 20:15:30 +10:00
dependabot[bot] 285282f366 chore(deps): bump minimist from 1.2.5 to 1.2.6 (#2457)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-18 12:53:56 +10:00
Sam Potts 02456d6ba3 Merge branch 'develop' 2022-04-18 11:44:11 +10:00
Sam Potts 1d920a2e58 chore: add cspell configuration 2022-04-18 11:43:57 +10:00
Jinhyeok Lee ba67920025 Fix 'this' while switching subtitle tracks (#2441)
When you switch a subtitle track you'll get

`TypeError: Cannot read properties of undefined (reading 'ui')` in `captions.js`'s `captions.setup`,

Because `captions.setup` is being called without proper `this` binding.
2022-04-18 11:43:44 +10:00
Hashen b7953ff0fc Add markers docs (#2439)
* change browserslist to cover 100% not dead browsers

* feat: add MediaMetadata

* Revert browserslist change

* add markers docs

Co-authored-by: Sam Potts <sam@potts.es>
2022-04-18 11:43:24 +10:00
CommanderRoot 6b8e0f25d6 Replace deprecated String.prototype.substr() (#2427)
String.prototype.substr() is deprecated (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr) so we replace it with slice() which works similarily but isn't deprecated.
Signed-off-by: Tobias Speicher <rootcommander@gmail.com>
2022-04-18 11:42:45 +10:00
Felix Klein 895299a4b3 modify vimeo parseHash to use non-named capture groups (#2426) 2022-04-18 11:40:19 +10:00
dependabot[bot] 2d7638a230 chore(deps): bump follow-redirects from 1.14.7 to 1.14.8 (#2422)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-18 11:39:32 +10:00
dependabot[bot] ab028ea475 chore(deps): bump nanoid from 3.1.30 to 3.2.0 (#2407)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.30 to 3.2.0.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.30...3.2.0)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-18 11:39:13 +10:00
Will Herring 9157ac09ed pass this context to captions.setup (#2399)
Co-authored-by: Will Herring <will.herring@eventcore.com>
2022-04-18 11:38:54 +10:00
Özgür Görgülü da7524438b Update speed option default to match the source (#2387)
Update speed option default value to match the source in README.md
2022-04-18 11:38:05 +10:00
Sam Potts c64c8ac6c0 Merge branch 'master' into develop 2022-04-18 11:37:12 +10:00
Sam Potts d700bb9f02 chore: add cspell configuration 2022-04-18 11:37:01 +10:00
toastal 2b6208565f SASS → Sass (#2436)
Sass is neither an acronym nor initialism. Sass also was never stylized
as SASS such as Less being stylized as both LESS and {less}.

http://www.sassnotsass.com/
2022-03-01 15:32:05 +11:00
Hashen 6bc447b916 feat: add MediaMetadata (#2410)
* change browserslist to cover 100% not dead browsers

* feat: add MediaMetadata

* Revert browserslist change

Co-authored-by: Sam Potts <sam@potts.es>
2022-02-24 23:42:58 +11:00
ForeverSc 4632614ced feat: add markers (#2386)
Co-authored-by: fengshuo <fengshuo@bilibili.com>
2022-02-12 20:35:52 +11:00
Sam Potts 01c5428fc4 Update README.md 2022-01-24 21:30:08 +11:00
dependabot[bot] 591c045c20 chore(deps): bump copy-props from 2.0.4 to 2.0.5 (#2401)
Bumps [copy-props](https://github.com/gulpjs/copy-props) from 2.0.4 to 2.0.5.
- [Release notes](https://github.com/gulpjs/copy-props/releases)
- [Changelog](https://github.com/gulpjs/copy-props/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gulpjs/copy-props/compare/2.0.4...2.0.5)

---
updated-dependencies:
- dependency-name: copy-props
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-15 17:20:19 +11:00
dependabot[bot] 1d7ea197f3 chore(deps): bump follow-redirects from 1.13.2 to 1.14.7 (#2400)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.13.2 to 1.14.7.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.13.2...v1.14.7)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-15 17:19:00 +11:00
Sam Potts 1d0d6a3a92 v3.6.12
- Fix: remove division logic from ads.scss (fixes #2370)
2021-12-17 07:45:19 +11:00
Sam Potts 6a92e60a67 fix: remove division logic from ads.scss (fixes #2370) 2021-12-17 07:39:58 +11:00
Sam Potts 40395a1ff5 v3.6.11
- Fix: Replace `list.slash` added in 3.6.10 with `calc`
- Chore: Package upgrades
- Chore: SASS clean up
- Chore: Improvements to style linting
2021-12-16 23:03:29 +11:00
Sam Potts 9ea3a6923d fix: use calc instead of list.slash 2021-12-16 22:59:44 +11:00
Sam Potts 675a853e2e chore: SASS clean up 2021-12-16 22:59:25 +11:00
Sam Potts 950786b117 chore: npm script syntax tweak 2021-12-16 22:58:58 +11:00
Sam Potts 626e1dba64 docs: fix typos 2021-12-16 22:56:46 +11:00
Sam Potts cbd1596af4 chore: stylelint config changes 2021-12-16 22:56:31 +11:00
Sam Potts 6ea510c36e chore: package upgrades 2021-12-16 22:56:13 +11:00
Sam Potts 0acf228dcd v3.6.10
- Fix: Use `list.slash` instead of deprecated syntax
- Chore: Clean up demo
2021-12-16 20:51:31 +11:00
Sam Potts 91b19baf44 fix: use list.slash instead of deprecated syntax 2021-12-16 20:29:36 +11:00
Sam Potts 990d636019 chore: clean up demo 2021-12-16 20:29:36 +11:00
Goodness Ezeh b9c792b377 Fix repeated "an" (#2371) 2021-11-28 22:51:21 +11:00
Sam Potts 1b835cb657 Merge branch 'master' into develop
# Conflicts:
#	src/js/plugins/vimeo.js
2021-10-13 21:53:13 +11:00
Sam Potts d9f9dc770b 3.6.9 2021-10-13 21:48:22 +11:00
Sam Potts dca2ff8cfd chore: linting 2021-10-13 21:45:30 +11:00
Sam Potts cf8e9341f2 fix: revert math.div SASS fallback 2021-09-29 22:07:42 +10:00
Sam Potts 5d1d247491 chore: minor code style tweak 2021-09-29 21:44:15 +10:00
Sam Potts fbf4d27cd4 chore: fix SASS build 2021-09-29 21:44:02 +10:00
Sam Potts e0b70d53cd fix: wrap localStorage sets in try/catch 2021-09-29 21:43:52 +10:00
Sam Potts 9f7725cec1 chore: package upgrades 2021-09-29 21:43:09 +10:00
Sam Potts 720cddff21 fix: add SASS div method fallback 2021-09-29 21:42:41 +10:00
Denis 1c33098c42 (fix): youtube videos duration (ref #2223). (#2224)
Co-authored-by: Denis Semionov <denis.semionov@prewise.com>
2021-09-29 21:14:43 +10:00
Frosch b256c102e8 Vimeo private videos: check for hash in src and add as a param (#2322)
* check for hash in src and add as a param

* Enable different methods of passing hash
2021-09-29 21:14:43 +10:00
Sam Potts a3716fc491 chore: update node version 2021-09-29 21:14:43 +10:00
Sam Potts 14e276a11d fix: video height issue 2021-09-29 21:14:43 +10:00
Björn Brändewall fc85a63af4 Fix invalid CSS selector syntax (#2303)
https://jigsaw.w3.org/css-validator/ complains that "The selector :empty can't appear after the pseudo-element selector ::after". Switching their places, however, will validate.
2021-09-29 21:14:43 +10:00
Emilis (Idea IT) f016a367b5 Fixed errors when Plyr instance is destroyed before constructor setTimeout() functions execute. (#2108)
Co-authored-by: Emilis Dambauskas <emilis.dambauskas@mps.fi>
2021-09-29 21:14:43 +10:00
Sam Potts d41a90f0dd change fullscreen element to player rootnode (#2204)
Co-authored-by: Walter van den Houten <63712489+wdfvdhouten@users.noreply.github.com>
2021-09-29 21:10:54 +10:00
Denis b4f24f90da (fix): youtube videos duration (ref #2223). (#2224)
Co-authored-by: Denis Semionov <denis.semionov@prewise.com>
2021-09-29 21:08:12 +10:00
Tony Narlock 9d751f4265 README: Link to svelte project (#2242) 2021-09-29 21:07:45 +10:00
Benny739 ff2c224254 Added preview thumbnails setter & fixed cannot set property 'hover' of null (#2256)
* added iPad to isIos

* elements.controls can be null

* elements.controls can be null

* added previewThumbnails setter

* fixed readme formatting

* previewThumbnails setter to method
2021-09-29 21:07:15 +10:00
AntLevin 1135b1acac Fix types - add tagUrl in ads option with custom VAST (#2319) 2021-09-29 21:05:56 +10:00
Hemratna Bhimani 30ffb30a97 Fixes iOS fullscreen not centered issue. (#2284) 2021-09-29 21:05:15 +10:00
Frosch e4a18a5c99 Vimeo private videos: check for hash in src and add as a param (#2322)
* check for hash in src and add as a param

* Enable different methods of passing hash
2021-09-29 21:04:56 +10:00
dependabot[bot] d1a10386c0 chore(deps): bump ws from 7.4.3 to 7.4.6 (#2226)
Bumps [ws](https://github.com/websockets/ws) from 7.4.3 to 7.4.6.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.4.3...7.4.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-25 00:13:41 +10:00
Lynn 4d6ac4ade1 chore: update plyr.d.ts for right ts check (#2245) 2021-08-24 23:18:09 +10:00
Andre Gagnon c2bfe489ea Let icons load in iframes without a location (#2259)
Currently, icons will fail to load if the player is placed inside an iframe without a window.location. This adds an additional check to use `window.top.location` if the window does not have a location.
2021-08-24 23:16:30 +10:00
Jack G 882952bba6 fix: fixed captions when switching source (#2261) 2021-08-24 23:15:45 +10:00
Roland 784f36921b Update video.scss (#2296) 2021-08-24 22:30:13 +10:00
dependabot[bot] b7dbdc133e chore(deps): bump path-parse from 1.0.6 to 1.0.7 (#2301)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-24 22:28:42 +10:00
Björn Brändewall 02d06c464c Fix invalid CSS selector syntax (#2303)
https://jigsaw.w3.org/css-validator/ complains that "The selector :empty can't appear after the pseudo-element selector ::after". Switching their places, however, will validate.
2021-08-24 22:28:02 +10:00
Emilis (Idea IT) cec2474295 Fixed errors when Plyr instance is destroyed before constructor setTimeout() functions execute. (#2108)
Co-authored-by: Emilis Dambauskas <emilis.dambauskas@mps.fi>
2021-08-24 22:24:50 +10:00
dependabot[bot] 6caa2f3fed chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9 (#2268)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

---
updated-dependencies:
- dependency-name: hosted-git-info
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-06 08:23:00 +10:00
Sam Potts 3a01837561 fix: allow local builds without git 2021-07-06 08:12:51 +10:00
Sam Potts ba4bdb335a docs: update placehold.it references 2021-07-06 08:11:23 +10:00
Sam Potts ba236c4753 v3.6.8 2021-05-12 23:40:37 +10:00
Sam Potts a204f7c1dd Merge branch 'develop' 2021-05-12 23:37:34 +10:00
Sam Potts bacf9122de chore: update nvmrc to 14 2021-05-12 23:37:05 +10:00
Sam Potts 2fdc1eac94 chore: lock file 2021-05-12 23:37:05 +10:00
Sam Potts 5fd4391cd9 fix: fullscreen issues with Vimeo (fixes #2175) 2021-05-12 23:37:05 +10:00
megumin 6991358428 Add container to FullscreenOptions (#2161) 2021-05-12 23:35:01 +10:00
Benny739 b050fde276 added iPad to isIos (#2191) 2021-05-12 23:33:48 +10:00
Sam Potts 32444c9851 v3.6.7
- Fix: remove regression caused by optional chaining and nullish coalescing in check for `window.CSS` check for aspect-ratio (fixes #2174)
2021-04-20 08:28:23 +10:00
Sam Potts b93dcc43ad fix: minor syntax tweak 2021-04-19 21:16:30 +10:00
Sam Potts 6ff6ff1673 fix: syntax fix (#2176) 2021-04-19 21:13:06 +10:00
Sam Potts e1ff86820c v3.6.6
- Improvements to how aspect ratio is handled. Use `aspect-ratio` CSS property instead of the legacy method (still used as fallback). Also automatically determined aspect ratios are rounded to the nearast standard ratio. This fixes issues with the YouTube embeds showing a 1-2px black bar.
- Hide the YouTube poster image container when paused so that the controls underneath can be used.
2021-04-18 17:49:23 +10:00
Sam Potts 7d22c361d1 chore: remove fastly purging logic (#2173) 2021-04-18 17:41:58 +10:00
Sam Potts 951cccb6b0 fix: youtube poster allow clickthrough while paused (#2172) 2021-04-18 16:59:00 +10:00
Sam Potts 438e425838 fix: aspect ratio improvements (#2171)
- Use CSS aspect-ratio (retain fallback for legacy browsers)
- Round aspect ratios (fixes YouTube black border issue)
2021-04-18 16:58:44 +10:00
Sam Potts ddb5ad071e chore: deploy 2021-04-17 00:30:30 +10:00
Sam Potts 38060d986c chore: release 2021-04-17 00:27:07 +10:00
Sam Potts 8c74396459 chore: linting 2021-04-17 00:27:07 +10:00
dependabot[bot] a09d63e8f8 chore(deps): bump is-svg from 4.2.1 to 4.3.1 (#2148)
Bumps [is-svg](https://github.com/sindresorhus/is-svg) from 4.2.1 to 4.3.1.
- [Release notes](https://github.com/sindresorhus/is-svg/releases)
- [Commits](https://github.com/sindresorhus/is-svg/compare/v4.2.1...v4.3.1)

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

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

* Fixing "missing code in detail" for PlyrEvent type

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

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

* Fix PreviewThumbnailsOptions type

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

* fix issue #1872

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

* Fix for Slow loading videos not autoplaying

* Fix for Slow loading videos not autoplaying

* Network requests are not cancelled after the player is destroyed

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

* chore: update packages and linting

* Invoke custom listener on triggering fullscreen via double-click

* Fix volume when unmuting from volume 0

* adding a nice Svelte plugin that I found

* Add missing unit to calc in media query

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

* Fix youtube not working when player is inside shadow dom

* v3.6.2

* ESLint to use common config

* add BitChute to users list

* Fix aspect ratio issue

* Revert noCookie change

* feat: demo radius tweaks

* fix: poster image shouldn’t receive click events

* chore: package updates

* chore: linting

* feat: custom controls option for embedded players

* Package upgrades

* ESLint to use common config

* Linting changes

* Update README.md

* chore: formatting

* fix: revert pointer events change for poster

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

* fix: demo using custom controls for YouTube

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

* Fixes #2005

* fix: overflowing volume slider

* chore: clean up CSS

* fix: hide poster when not using custom controls

* Package upgrades

* ESLint to use common config

* Linting changes

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

* docs: changelog for v3.6.3

* Remove unnecessary calc from media query (#2049)

* Enhance types (#1841)

* 🏷️(type) enhance QualityOptions type

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

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

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

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

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

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

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

* Fix to work inside iframes. (#2069)

* Fix to work inside iframes.

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

* Update is.js

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

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

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

* Use isVimeo-function instead of hardcoded value

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

* fix: use new syntax for iframe allow attribute

* Bump ini from 1.3.5 to 1.3.7 (#2044)

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

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

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

* chore: package updates

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

* fix: use bound arrow functions in classes

* chore: cleanup commented out code

* chore: release

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

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

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

* Use isVimeo-function instead of hardcoded value

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

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

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

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

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

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

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

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

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

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

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

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

* Fixing "missing code in detail" for PlyrEvent type

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

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

* Fix PreviewThumbnailsOptions type

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

* fix issue #1872

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

* Fix for Slow loading videos not autoplaying

* Fix for Slow loading videos not autoplaying

* Network requests are not cancelled after the player is destroyed

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

* chore: update packages and linting

* Invoke custom listener on triggering fullscreen via double-click

* Fix volume when unmuting from volume 0

* adding a nice Svelte plugin that I found

* Add missing unit to calc in media query

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

* Fix youtube not working when player is inside shadow dom

* v3.6.2

* ESLint to use common config

* add BitChute to users list

* Fix aspect ratio issue

* Revert noCookie change

* feat: demo radius tweaks

* fix: poster image shouldn’t receive click events

* chore: package updates

* chore: linting

* feat: custom controls option for embedded players

* Package upgrades

* ESLint to use common config

* Linting changes

* Update README.md

* chore: formatting

* fix: revert pointer events change for poster

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

* fix: demo using custom controls for YouTube

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

* Fixes #2005

* fix: overflowing volume slider

* chore: clean up CSS

* fix: hide poster when not using custom controls

* Package upgrades

* ESLint to use common config

* Linting changes

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

* docs: changelog for v3.6.3

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-16 18:00:08 +00:00
Sam Potts 423b7b276f Merge pull request #1900 from iwatakeshi/patch-1
Fix PreviewThumbnailsOptions type
2020-07-14 09:03:36 +10:00
Takeshi cbd4a7cef4 Fix PreviewThumbnailsOptions type
According to the docs, the `src` should also accept an array of strings.
2020-07-13 13:00:16 -06:00
Sam Potts 391afe2814 Merge pull request #1889 from rutheneum-bote/patch-1
Add us to the "Used by" list
2020-06-30 22:08:25 +10:00
Rutheneum-Bote 68e3944bb8 Add us to the "Used by" list
We're the students' newspaper of the Goethe Grammar School/Rutheneum since 1608 and we use Plyr in order to present the students' songs that were submitted to our school's creativity contest.
2020-06-30 11:47:09 +00:00
Honoré Hounwanou 2ee6ae16b2 Fix small typo
Add missing "d" in video.
2020-06-27 10:18:49 -04:00
Jonathan Arbely 8ce7425f73 Update link to working dash.js demo (was broken) 2020-06-09 20:13:41 +02:00
akuma06 1f63806c3f Fixing "missing code in detail" for PlyrEvent type
When using typescript and listening for youtube statechange event, it is missing the code property definition inside the event (even though it is provided in the code).
By making events a map of key-value, we can add easily custom event type for specific event name. Since YouTube "statechange" event differs from the basic PlyrEvent, I added a new Event Type "PlyrStateChangeEvent" having a code property corresponding to a YoutubeState enum defined by the YouTube API documentation.
This pattern follows how addEventListener in the lib.dom.d.ts is defined.
2020-05-05 15:34:41 +02:00
Sam Potts 6cb822d56f Merge pull request #1823 from theprojectsomething/features/fullscreen-container
force fullscreen events to trigger on plyr element
2020-05-05 17:07:00 +10:00
Som Meaden 9dee5acec6 force fullscreen events to trigger on plyr element (media element in iOS) and not fullscreen container 2020-05-05 16:35:36 +10:00
Sam Potts ef85cb6195 Merge pull request #1822 from zoomerdev/patch-1
Add BitChute to users list
2020-05-05 15:26:29 +10:00
zoomerdev dc8612fcd0 add BitChute to users list 2020-05-05 01:19:42 -04:00
Sam Potts 86c3cf648f v3.6.2 2020-05-04 21:38:10 +10:00
Sam Potts 37be1336fb v3.6.2 2020-05-04 21:33:16 +10:00
Sam Potts 3d7f80737b Package upgrades 2020-05-04 21:32:30 +10:00
Sam Potts 58a9321764 Merge pull request #1818 from Bashev/patch-1
Update tooltips.scss
2020-05-04 21:22:21 +10:00
Sam Potts b600f387f0 Merge pull request #1819 from Bashev/patch-2
Update controls.scss
2020-05-04 21:22:05 +10:00
Kostadin Bashev 2a97adbec6 Update controls.scss 2020-05-01 18:17:49 +03:00
Kostadin Bashev 853753a3da Update tooltips.scss 2020-05-01 18:16:59 +03:00
Sam Potts 405bf9ce37 Merge pull request #1815 from taylorchu/progbar
add missing previewThumbnails options
2020-04-29 10:10:39 +10:00
taylorchu d5ea881729 add missing previewThumbnails options 2020-04-28 17:03:10 -07:00
Sam Potts 391b9acd98 Merge pull request #1811 from taylorchu/better-control-2
allow custom control as element
2020-04-29 09:39:50 +10:00
Sam Potts dda2d072aa Merge pull request #1813 from sampotts/master
Merge back
2020-04-29 09:21:32 +10:00
taylorchu 3c1ba2397b allow custom control as element 2020-04-28 15:28:36 -07:00
Sam Potts adb3f35920 v3.6.1 2020-04-28 23:17:54 +10:00
Sam Potts a58b8bf4bb 2 spaces 2020-04-28 22:22:17 +10:00
Sam Potts 99a26d65cf Merge pull request #1796 from sampotts/develop
v3.6.0
2020-04-28 22:20:49 +10:00
Sam Potts 4915cf0120 Use new feature to set thumbnails on source change in the demo 2020-04-24 01:15:54 +10:00
Sam Potts 66c5780616 v3.6.0 2020-04-24 01:05:58 +10:00
Sam Potts e48b1d11ce Housekeeping 2020-04-24 00:47:41 +10:00
Sam Potts ba91f23c50 Fix linting issues 2020-04-24 00:39:26 +10:00
Sam Potts ad4f303aa0 Merge branch 'master' into develop
# Conflicts:
#	readme.md
2020-04-24 00:34:44 +10:00
Sam Potts 3b521f73bd Merge branch 'develop' of github.com:sampotts/plyr into develop
# Conflicts:
#	package.json
#	yarn.lock
2020-04-24 00:32:16 +10:00
Sam Potts 27126b20bc Merge 2020-04-24 00:31:50 +10:00
Sam Potts 68137aa789 Merge pull request #1770 from sampotts/css-variables
Allow customization via CSS Custom Properties
2020-04-24 00:22:39 +10:00
Sam Potts 2458eaa11b Merge branch 'develop' into css-variables
# Conflicts:
#	src/js/captions.js
#	src/js/config/defaults.js
#	src/js/fullscreen.js
#	src/js/listeners.js
#	src/js/plyr.js
2020-04-24 00:22:17 +10:00
Sam Potts a97008aeeb More work on custom properties and documentation 2020-04-24 00:14:50 +10:00
Sam Potts a9b24f5e1f Use custom properties in demo 2020-04-23 22:07:32 +10:00
Sam Potts a9c4e77d1b Migrate custom properties from media to parent 2020-04-23 22:06:36 +10:00
Sam Potts dbe618c644 Merge pull request #1793 from theprojectsomething/features/postload-captions
Preload TextTracks as per default video element
2020-04-23 22:03:45 +10:00
Som Meaden 40f06e0b4c This commit addresses preloading TextTracks as outlined in feature request #1791
These changes bring Plyr captions download behaviour in line with that of the default video element in major browsers. Specifically text tracks only download as they are required for display. Previously all text tracks would download when the Plyr instance was instantiated - which could become an issue when e.g. many translations are available.

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-28 22:38:57 +00:00
Sascha Greuel e978bc8690 Fixed ads configuration 2019-08-27 19:28:51 +02:00
Thomas Skerbis 91503d3698 Update readme.md 2019-08-15 21:55:22 +02:00
Thomas Skerbis 1721f6e9e2 Update readme.md 2019-08-15 21:53:41 +02:00
Manuel Raynaud c7b5aa9197 ️(controls) change play button aria-label value when its state change
The aria-label attribute set on all play buttons does not change
according the player state. When the video is playing, the aria-label
should change to pause otherwise screen reader will not detect that this
button now can be used to pause the video.
2019-08-07 18:00:02 +02:00
ondratra 800c8e0a17 typescript typings 2019-08-07 00:08:10 +02:00
Aziz Khambati d771da9abf Toggle also returns promise 2019-07-30 18:19:10 +05:30
nskazki b36b92b247 Detach event listeners on destroy
if "on" receives "this", it attaches "eventListeners" array to "this" and stores "{ element, type, callback }" hash in there.
later, during the destruction process, all the entries from this array will be processed by "unbindListeners" helper to detach all the event listeners.
2019-07-19 16:26:01 +03:00
Sam Potts aa51719a55 Merge pull request #1497 from ffpetrovic/patch-1
Update index.html
2019-07-12 08:46:42 +10:00
Filip Petrovic a28685536a Update index.html 2019-07-11 23:18:35 +02:00
Antony O'Neill 400fd77d0a Prevent default on settings icon click 2019-07-04 19:02:22 +01:00
MaxGiting a2bf974058 Improve clarity 2019-07-01 15:09:29 +01:00
Sam Potts 7c442c9357 3.5.6 2019-06-21 12:35:47 +10:00
Sam Potts e17e0a81dd Package upgrade 2019-06-21 12:35:22 +10:00
Sam Potts 2488299d7b Edge fix 2019-06-21 12:34:49 +10:00
Sam Potts dfc09b8e04 v3.5.5 deployed 2019-06-21 00:24:28 +10:00
Sam Potts 6438baaddc Merge branch 'develop' 2019-06-21 00:19:51 +10:00
Sam Potts 8fc6c2ba52 File rename and clean up 2019-06-21 00:19:37 +10:00
Sam Potts 95092edc93 Merge pull request #1472 from sampotts/develop
v3.5.5
2019-06-21 00:12:10 +10:00
Sam Potts c4b3e0672e Clean up 2019-06-21 00:10:57 +10:00
Sam Potts e8e2b8ba39 Merge branch 'master' into develop
# Conflicts:
#	.eslintrc
#	demo/dist/demo.css
#	demo/dist/demo.js
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.css
#	dist/plyr.js
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.min.mjs
#	dist/plyr.min.mjs.map
#	dist/plyr.mjs
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	dist/plyr.polyfilled.min.mjs
#	dist/plyr.polyfilled.min.mjs.map
#	dist/plyr.polyfilled.mjs
#	package.json
#	readme.md
#	src/js/listeners.js
#	yarn.lock
2019-06-20 23:56:19 +10:00
Sam Potts 2e40b91ec1 Styling tweaks for demo 2019-06-20 23:50:46 +10:00
Sam Potts 97d9228bed Aspect ratio tweaks 2019-06-03 20:13:16 +10:00
Sam Potts 0f14865d56 Add duration (commented out) in defaults 2019-06-03 20:12:43 +10:00
Sam Potts c94ab2a39f Repaint clean up 2019-06-03 20:12:21 +10:00
Sam Potts ab89e055de Demo tweaks 2019-06-03 20:11:31 +10:00
Sam Potts ac6e3dba5a Fix for thumbnails in demo for audio 2019-06-03 00:28:09 +10:00
Sam Potts d9d2c4a219 Demo tweaks 2019-06-03 00:26:08 +10:00
Sam Potts 1890a9378d Gulp tweaks 2019-06-02 23:16:45 +10:00
Sam Potts ac88e6e190 Demo clean up 2019-06-02 23:16:29 +10:00
Sam Potts 15cbae8a19 Removed commented out code for Edge 2019-06-02 22:25:44 +10:00
Sam Potts aaf096d96d Removed raven-js from dependencies 2019-06-02 22:25:17 +10:00
Sam Potts 93f5acbffd Fixed cite display 2019-06-02 22:25:00 +10:00
Sam Potts 9c717275d2 Packages for demo separated 2019-06-02 22:24:41 +10:00
Sam Potts 50a7c2fad6 Build 2019-06-01 20:01:24 +10:00
Sam Potts 95be6e721b Fixed merge 2019-06-01 20:01:19 +10:00
Sam Potts dc2e012cc9 Clean up 2019-06-01 20:01:09 +10:00
Sam Potts 12c6282d14 Merge branch 'develop' into css-variables
# Conflicts:
#	.eslintrc
#	demo/dist/demo.css
#	demo/dist/demo.js
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.css
#	dist/plyr.js
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.min.mjs
#	dist/plyr.min.mjs.map
#	dist/plyr.mjs
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	dist/plyr.polyfilled.min.mjs
#	dist/plyr.polyfilled.min.mjs.map
#	dist/plyr.polyfilled.mjs
#	gulpfile.js
#	package.json
2019-06-01 19:55:14 +10:00
Sam Potts 0249772f01 Clean up 2019-06-01 19:50:29 +10:00
Sam Potts 5d699d5f47 Merge branch 'develop' of github.com:sampotts/plyr into develop 2019-06-01 19:29:21 +10:00
Sam Potts 34d79a5443 Merge pull request #1453 from aFarkas/develop
youtube multiple small issues
2019-06-01 18:46:56 +10:00
Sam Potts 1e761e237a Merge pull request #1458 from Jason-Cooke/patch-1
docs: fix typo
2019-06-01 18:46:14 +10:00
Sam Potts 36ad132c82 Edge fix 2019-06-01 18:45:12 +10:00
Sam Potts c9055f391b Linting changes 2019-06-01 18:45:07 +10:00
Jason Cooke 99c95363b4 docs: fix typo 2019-06-01 11:13:44 +12:00
Sam Potts c90526bf07 Create FUNDING.yml 2019-05-27 17:17:05 +10:00
Alexander Farkas 97a6e72e10 fix youtube embed + handle early destroy 2019-05-24 16:55:45 +02:00
Alexander Farkas f100caba81 fix youtube embed + handle early destroy 2019-05-24 16:53:58 +02:00
Sam Potts 80aa6ffe43 Linting changes 2019-04-30 23:44:05 +10:00
Sam Potts 0694e58650 v3.5.4 2019-04-25 12:14:48 +10:00
Sam Potts e644eeb5b6 Merge pull request #1423 from sampotts/develop
v3.5.4
2019-04-25 12:11:06 +10:00
Sam Potts 5ddd9e02de Fix for the menu in the shadow DOM 2019-04-25 12:09:46 +10:00
Sam Potts a064f0b4fd Merge branch 'develop' of github.com:sampotts/plyr into develop 2019-04-25 12:04:38 +10:00
Sam Potts b9dbcc30fa Eslint config rename 2019-04-25 12:03:42 +10:00
Sam Potts 7ab34c7d2e Changelog 2019-04-25 12:03:32 +10:00
Sam Potts 17caa3c57b Fix merging class 2019-04-25 12:03:24 +10:00
Sam Potts 2e6361898b Package updates 2019-04-25 12:03:13 +10:00
Sam Potts a1b2c0419f Ads improvements for volume and race condition fix 2019-04-25 12:03:04 +10:00
Sam Potts 9b81e776fb Styling for control changes 2019-04-25 12:02:48 +10:00
Sam Potts 7a4a7dece8 Ratio improvements 2019-04-25 12:02:37 +10:00
Sam Potts f0d3e8c3b9 Fix for className being wiped out 2019-04-25 12:01:52 +10:00
Sam Potts ad68d9484f Clean up and API change 2019-04-25 12:01:30 +10:00
Sam Potts c3fd822857 Notes on autoplay support 2019-04-25 12:01:15 +10:00
Sam Potts e147f3a754 Formatting 2019-04-25 12:01:00 +10:00
Sam Potts b694e7d3ab Use polyfill.io v3 2019-04-22 12:28:26 +10:00
Sam Potts b2fff4c33f Increase speed limits 2019-04-15 22:08:09 +10:00
Sam Potts 9c1060d9b0 Merge pull request #1416 from emielbeinema/fix_webcomponents
Make menu work in WebComponent
2019-04-15 21:41:23 +10:00
Emiel Beinema a91652287b Don't close menu on click in menu in webcomponent 2019-04-15 12:17:10 +02:00
Emiel Beinema 2cf44c236d Use querySelector on container for showMenuPanel 2019-04-15 12:16:48 +02:00
Sam Potts 243db9eda3 Fix issue with empty controls and preview thumbs 2019-04-13 12:56:15 +10:00
Sam Potts b675ba1f35 Fix issue with setGutter call 2019-04-13 12:55:48 +10:00
Sam Potts e9367ee85e Fix setting initial speed (fixes #1408) 2019-04-12 20:18:17 +10:00
Sam Potts d9b7928ce6 Merge branch 'master' into develop 2019-04-12 19:00:23 +10:00
Sam Potts cdacae6697 Set download URL via setter 2019-04-12 19:00:17 +10:00
Sam Potts 2bd08cdc28 3.5.3 2019-04-12 18:44:05 +10:00
Sam Potts 5fefabe3bd Merge pull request #1410 from sampotts/develop
v3.5.3
2019-04-12 18:39:14 +10:00
Sam Potts e281078441 Bump version 2019-04-12 18:38:46 +10:00
Sam Potts 9bb75f6f52 Changelog 2019-04-12 18:37:11 +10:00
Sam Potts 3b7a24456d Package upgrades 2019-04-12 18:36:55 +10:00
Sam Potts c885d59270 Moved all video styles together 2019-04-12 12:43:45 +10:00
Sam Potts 9f30d54963 Merge branch 'master' into develop
# Conflicts:
#	readme.md
2019-04-12 12:40:12 +10:00
Sam Potts b247093495 Aspect ratio improvements (fixes #1042, fixes #1366) 2019-04-12 12:19:48 +10:00
Sam Potts 9ca7b861a9 Autoplay tweak for HTML5 2019-04-12 12:14:12 +10:00
Sam Potts 2eccf0dd05 Fix YouTube autoplay (fixes #1185) 2019-04-12 12:13:46 +10:00
Sam Potts 566b957e65 Merge pull request #1362 from doublex/master
#46 - two patches from 'jamesoflol'
2019-04-11 21:20:23 +10:00
Sam Potts a8456f4ca7 Merge pull request #1404 from taion/clear-timeouts
fix: Properly clear all timeouts on destroy
2019-04-11 21:18:52 +10:00
Sam Potts 0f3098040d Merge pull request #1407 from freezer278/http-youtube-fix
fixed setting youtube host for non-https case
2019-04-11 21:18:15 +10:00
Sam Potts 996075decc More work on variable usage 2019-04-11 20:50:20 +10:00
Vladimir Morozov 21539be3f2 code cleanup 2019-04-04 09:32:38 +03:00
Vladimir Morozov c22f5c4b39 code cleanup 2019-04-04 08:56:46 +03:00
Vladimir Morozov f4b47a9275 fixed setting youtube host for non-https case 2019-04-04 08:51:20 +03:00
Jimmy Jia 266b70d9d0 fix: Properly clear all timeouts on destroy 2019-04-01 14:42:51 -04:00
Sam Potts 6e68ad6d15 Update readme.md 2019-03-26 21:43:59 +11:00
Sam Potts 848e798809 Fix merge 2019-03-16 12:15:20 +11:00
Sam Potts 35f7ee9c59 Merge branch 'develop' into css-variables
# Conflicts:
#	demo/dist/demo.css
#	demo/index.html
#	dist/plyr.css
#	gulpfile.js
#	package.json
#	yarn.lock
2019-03-16 12:14:20 +11:00
Sam Potts c202551e6d Merge branch 'develop' of github.com:sampotts/plyr into develop 2019-03-16 11:58:12 +11:00
Sam Potts 5b7a025d26 Housekeeping 2019-03-16 11:57:15 +11:00
Sam Potts 26bcf83960 Merge pull request #1376 from ar31an/patch-1
Update poster src
2019-03-07 18:59:58 +11:00
Arslan Javed e4acff4f8d Update poster src 2019-03-07 12:54:07 +05:00
Sam Potts 568ddf2390 Merge pull request #1375 from sampotts/master
Merge back
2019-03-07 18:30:05 +11:00
Your Name ce91945544 Preview seek: optional hours and ms in VTT parser 2019-02-27 15:45:24 +01:00
Your Name 11fed8d1b5 Preview seek: fix: allow absolute thumbnail paths 2019-02-27 15:43:36 +01:00
Sam Potts bdd513635f Work on outline/focus styles 2018-12-08 17:06:20 +11:00
Sam Potts b7b2e3c0aa Merge branch 'develop' into css-variables
# Conflicts:
#	demo/dist/demo.css
#	demo/dist/demo.js
#	demo/dist/demo.js.map
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.css
#	dist/plyr.js
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	gulpfile.js
#	src/sass/components/captions.scss
#	src/sass/components/control.scss
2018-11-11 11:05:09 +11:00
Sam Potts 3e0a911418 WIP 2018-05-26 13:37:10 +10:00
167 changed files with 27613 additions and 76316 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
+18
View File
@@ -0,0 +1,18 @@
{
"parser": "babel-eslint",
"extends": ["@sampotts/eslint-config/es6"],
"env": {
"browser": true,
"es6": true
},
"globals": {
"Plyr": false,
"jQuery": false
},
"rules": {
"import/no-cycle": "warn"
},
"parserOptions": {
"sourceType": "module"
}
}
-35
View File
@@ -1,35 +0,0 @@
{
"parser": "babel-eslint",
"extends": ["airbnb-base", "prettier"],
"env": {
"browser": true,
"es6": true
},
"globals": {
"Plyr": false,
"jQuery": false
},
"rules": {
"import/no-cycle": 1,
"no-const-assign": 1,
"no-shadow": 0,
"no-this-before-super": 1,
"no-undef": 1,
"no-unreachable": 1,
"no-unused-vars": 1,
"constructor-super": 1,
"valid-typeof": 1,
"indent": [2, 4, { "SwitchCase": 1 }],
"quotes": [2, "single", "avoid-escape"],
"semi": [2, "always"],
"eqeqeq": [2, "always"],
"one-var": [2, "never"],
"comma-dangle": [2, "always-multiline"],
"spaced-comment": [2, "always"],
"no-restricted-globals": 2,
"no-param-reassign": [2, { "props": false }]
},
"parserOptions": {
"sourceType": "module"
}
}
+5
View File
@@ -0,0 +1,5 @@
# These are supported funding model platforms
github: sampotts
patreon: plyr
open_collective: plyr
+1 -1
View File
@@ -1,3 +1,3 @@
PLEASE USE OUR SPECIFIC ISSUE TEMPLATES for bug reports, features and improvement suggestions.
Our issue tracker is not for support questions. If you need help, follow our support instructions: https://github.com/sampotts/plyr/blob/master/contributing.md#support
Our issue tracker is not for support questions. If you need help, follow our support instructions: https://github.com/sampotts/plyr/blob/master/CONTRIBUTING.md#support
-5
View File
@@ -1,8 +1,3 @@
### Link to related issue (if applicable)
### Summary of proposed changes
### Checklist
- [ ] Use `develop` as the base branch
- [ ] Exclude the gulp build (`/dist` changes) from the PR
- [ ] Test on [supported browsers](https://github.com/sampotts/plyr#browser-support)
+1 -3
View File
@@ -2,10 +2,8 @@ node_modules
.DS_Store
credentials.json
*.mp4
!dist/blank.mp4
index-*.html
npm-debug.log
yarn-error.log
package-lock.json
*.webm
.idea/
dist/
+6
View File
@@ -0,0 +1,6 @@
tasks:
- before: npm install && npm i gulp -g
command: gulp
ports:
- port: 3000
onOpen: open-preview
+1
View File
@@ -0,0 +1 @@
19.7.0
+2 -2
View File
@@ -7,6 +7,6 @@ credentials.json
deploy.json
yarn.lock
package-lock.json
*.mp4
*.webm
!dist/blank.mp4
*.mp4
!dist/blank.mp4
+5 -5
View File
@@ -1,7 +1,7 @@
{
"useTabs": false,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120
"useTabs": false,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120
}
+20 -22
View File
@@ -1,25 +1,23 @@
{
"plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"],
"extends": ["stylelint-config-recommended", "stylelint-config-sass-guidelines", "stylelint-config-prettier"],
"rules": {
"selector-class-pattern": null,
"selector-no-qualifying-type": [
true,
{
"ignore": ["attribute", "class"]
}
],
"string-no-newline": null,
"indentation": 4,
"string-quotes": "single",
"max-nesting-depth": 2,
"plugin/selector-bem-pattern": {
"preset": "bem",
"componentName": "(([a-z0-9]+(?!-$)-?)+)",
"componentSelectors": {
"initial": "\\.{componentName}(((__|--)(([a-z0-9\\[\\]'=]+(?!-$)-?)+))+)?$"
},
"ignoreSelectors": [".*\\.has-.*", ".*\\.is-.*"]
}
"plugins": ["stylelint-selector-bem-pattern"],
"extends": ["stylelint-config-sass-guidelines", "stylelint-config-prettier"],
"customSyntax": "postcss-scss",
"rules": {
"selector-class-pattern": null,
"selector-no-qualifying-type": [
true,
{
"ignore": ["attribute", "class"]
}
],
"max-nesting-depth": 4,
"plugin/selector-bem-pattern": {
"preset": "bem",
"componentName": "(([a-z0-9]+(?!-$)-?)+)",
"componentSelectors": {
"initial": "\\.{componentName}(((__|--)(([a-z0-9\\[\\]'=]+(?!-$)-?)+))+)?$"
},
"ignoreSelectors": [".*\\.has-.*", ".*\\.is-.*"]
}
}
}
-8
View File
@@ -1,8 +0,0 @@
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
View File
@@ -1,5 +0,0 @@
#!/bin/bash
if [ $TRAVIS_BRANCH == "develop" ] && $(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qE "^(demo/)?dist/"); then
echo 'Build output ("dist" and "demo/dist") not permitted in develop' >&2
exit 1
fi
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
if [ "$TRAVIS_PULL_REQUEST" != "false" ] && [ $TRAVIS_BRANCH == "master" ] && $(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -q "^src/"); then
echo 'The base branch for pull requests must be "develop"' >&2
exit 1
fi
+4 -4
View File
@@ -2,11 +2,11 @@
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
"dbaeumer.vscode-eslint",
"wix.vscode-import-cost",
"esbenp.prettier-vscode",
"shinnn.stylelint",
"wayou.vscode-todo-highlight"
"wayou.vscode-todo-highlight",
"wix.vscode-import-cost",
"stylelint.vscode-stylelint",
"pflannery.vscode-versionlens"
]
}
+1323
View File
File diff suppressed because it is too large Load Diff
+25 -11
View File
@@ -7,15 +7,17 @@ We welcome bug reports, feature requests and pull requests. If you want to help
Before asking questions, read our [documentation](https://github.com/sampotts/plyr) and [FAQ](https://github.com/sampotts/plyr/wiki/FAQ).
If these doesn't answer your question
* Use [Stack Overflow](https://stackoverflow.com/) for questions that doesn't directly involve Plyr. This includes for example how to use Javascript, CSS or HTML5 media in general, and how to use other frameworks, libraries and technology.
* Use [our Slack](https://bit.ly/plyr-chat) if you need help using Plyr or have questions about Plyr.
- Use [Stack Overflow](https://stackoverflow.com/) for questions that doesn't directly involve Plyr. This includes for example how to use Javascript, CSS or HTML5 media in general, and how to use other frameworks, libraries and technology.
- Use [our Slack](https://bit.ly/plyr-chat) if you need help using Plyr or have questions about Plyr.
## Commenting
When commenting, keep a civil tone and stay on topic. Don't ask for [support](#support), or post "+1" or "I agree" type of comments. Use the emojis instead.
Asking for the status on issues is discouraged. Unless someone has explicitly said in an issue that it's work in progress, most likely that means no one is working on it. We have a lot to do, and it may not be a top priority for us.
We *may* moderate discussions. We do this to avoid threads being "hijacked", to avoid confusion in case the content is misleading or outdated, and to avoid bothering people with github notifications.
We _may_ moderate discussions. We do this to avoid threads being "hijacked", to avoid confusion in case the content is misleading or outdated, and to avoid bothering people with github notifications.
## Creating issues
@@ -23,18 +25,30 @@ Please follow the instructions in our issue templates. Don't use github issues t
## Contributing features and documentation
* If you want to add a feature or make critical changes, you may want to ensure that this is something we also want (so you don't waste your time). Ask us about this in the corresponding issue if there is one, or on [our Slack](https://bit.ly/plyr-chat) otherwise.
- If you want to add a feature or make critical changes, you may want to ensure that this is something we also want (so you don't waste your time). Ask us about this in the corresponding issue if there is one, or on [our Slack](https://bit.ly/plyr-chat) otherwise.
* Fork Plyr, and create a new branch in your fork, based on the **develop** branch
- Fork Plyr, and create a new branch in your fork, based on the **develop** branch
* To test locally, you can use the demo. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build while you are working, and run a local server from the repository root directory. If you have Python installed, this command should work: `python -m SimpleHTTPServer 8080`. Then go to `http://localhost:8080/demo/`
- To test locally, you can use the demo site. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build and it will run a local web server for development and watch for any changes.
* Develop and test your modifications.
### Online one-click setup for contributing
* Preferably commit your changes as independent logical chunks, with meaningful messages. Make sure you do not commit unnecessary files or changes, such as the build output, or logging and breakpoints you added for testing.
You can use Gitpod (a free online VS Code-like IDE) for contributing. With a single click it will launch a workspace and automatically:
* If your modifications changes the documented behavior or add new features, document these changes in readme.md.
- clone the plyr repo.
- install the dependencies with `yarn install` in root directory and "demo" directory.
- run `gulp` in root directory to start the dev server.
* When finished, push the changes to your GitHub repository and send a pull request to **develop**. Describe what your PR does.
So that you can start straight away.
* If the Travis build fails, or if you get a code review with change requests, you can fix these by pushing new or rebased commits to the branch.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/from-referrer/)
- Develop and test your modifications.
- Preferably commit your changes as independent logical chunks, with meaningful messages. Make sure you do not commit unnecessary files or changes, such as the build output, or logging and breakpoints you added for testing.
- If your modifications changes the documented behavior or add new features, document these changes in [README.md](README.md).
- When finished, push the changes to your GitHub repository and send a pull request. Describe what your PR does.
- If the Travis build fails, or if you get a code review with change requests, you can fix these by pushing new or rebased commits to the branch.
+27 -27
View File
@@ -2,11 +2,11 @@
This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs. You can pass the following to the `controls` option:
- `Array` of options (this builds the default controls based on your choices)
- `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
- `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
@@ -14,28 +14,28 @@ If you want to use the standard controls as they are, you don't need to pass any
```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)
'download', // Show a download button with a link to either the current source or a custom URL you specify in your options
'fullscreen', // Toggle fullscreen
'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)
'download', // Show a download button with a link to either the current source or a custom URL you specify in your options
'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.
You can provide an `i18n` object as one of your options when initializing the plugin which we be used when rendering the controls.
#### Example
@@ -84,14 +84,14 @@ The classes and data attributes used in your template should match the `selector
You need to add several placeholders to your HTML template that are replaced when rendering:
- `{id}` - the dynamically generated ID for the player (for form controls)
- `{seektime}` - the seek time specified in options for fast forward and rewind
- `{title}` - the title of your media, if specified
- `{id}` - the dynamically generated ID for the player (for form controls)
- `{seektime}` - the seek time specified in options for fast forward and rewind
- `{title}` - the title of your media, if specified
### Limitations
- Currently the settings menus are not supported with custom controls HTML
- AirPlay and PiP buttons can be added but you will have to manage feature detection
- Currently the settings menus are not supported with custom controls HTML
- AirPlay and PiP buttons can be added but you will have to manage feature detection
### Example
View File
+445 -285
View File
@@ -1,36 +1,40 @@
| 🎉 | [Plyr is merging into Vidstack](https://github.com/sampotts/plyr/issues/2408) | 🎉 |
| :-: | :---------------------------------------------------------------------------: | :-- |
Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vimeo media player that supports [_modern_](#browser-support) browsers.
[Checkout the demo](https://plyr.io) - [Donate](#donate) - [Slack](https://bit.ly/plyr-chat) - [![npm version](https://badge.fury.io/js/plyr.svg)](https://badge.fury.io/js/plyr)
[Checkout the demo](https://plyr.io) - [Donate](#donate) - [Slack](https://bit.ly/plyr--chat)
[![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png?v=3)](https://plyr.io)
[![npm version](https://badge.fury.io/js/plyr.svg)](https://badge.fury.io/js/plyr) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/sampotts/plyr) [![Financial Contributors on Open Collective](https://opencollective.com/plyr/all/badge.svg?label=financial+contributors)](https://opencollective.com/plyr)
[![Screenshot of Plyr](https://cdn.plyr.io/static/screenshot.webp)](https://plyr.io)
# Features
- 💪 **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](#demos)** - 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 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
- 👌 **[Preview thumbnails](#preview-thumbnails)** - support for displaying preview thumbnails
- 🤟 **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required
- 💁‍♀️ **SASS** - to include in your build processes
- 📼 **HTML Video & Audio, YouTube & Vimeo** - support for the major formats
- 💪 **Accessible** - full support for VTT captions and screen readers
- 🔧 **[Customizable](#html)** - make the player look how you want with the markup you want
- 😎 **Clean HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
`<span>` or `<a href="#">` button hacks
- 📱 **Responsive** - works with any screen size
- 💵 **[Monetization](#ads)** - make money from your videos
- 📹 **[Streaming](#demos)** - 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 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
- 👌 **[Preview thumbnails](#preview-thumbnails)** - support for displaying preview thumbnails
- 🤟 **No frameworks** - written in "vanilla" ES6 JavaScript, no jQuery required
- 💁‍♀️ **Sass** - to include in your build processes
### Demos
## Demos
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)
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=GRoogML), [Hls.js](https://codepen.io/pen?template=oyLKQb) and [Shaka Player](https://codepen.io/pen?template=ZRpzZO)
# Quick setup
@@ -41,21 +45,23 @@ Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.o
### HTML5 Video
```html
<video poster="/path/to/poster.jpg" id="player" playsinline controls>
<source src="/path/to/video.mp4" type="video/mp4" />
<source src="/path/to/video.webm" type="video/webm" />
<video id="player" playsinline controls data-poster="/path/to/poster.jpg">
<source src="/path/to/video.mp4" type="video/mp4" />
<source src="/path/to/video.webm" type="video/webm" />
<!-- Captions are optional -->
<track kind="captions" label="English captions" src="/path/to/captions.vtt" srclang="en" default />
<!-- Captions are optional -->
<track kind="captions" label="English captions" src="/path/to/captions.vtt" srclang="en" default />
</video>
```
**Note**: The poster image should be specified using `data-poster`. This is to prevent it [being downloaded twice](https://github.com/sampotts/plyr/issues/1531). If you're sure the image will be cached, you can still use the `poster` attribute for true progressive enhancement.
### HTML5 Audio
```html
<audio id="player" controls>
<source src="/path/to/audio.mp3" type="audio/mp3" />
<source src="/path/to/audio.ogg" type="audio/ogg" />
<source src="/path/to/audio.mp3" type="audio/mp3" />
<source src="/path/to/audio.ogg" type="audio/ogg" />
</audio>
```
@@ -67,12 +73,12 @@ We recommend [progressive enhancement](https://www.smashingmagazine.com/2009/04/
```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>
<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>
```
@@ -92,12 +98,12 @@ 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>
<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>
```
@@ -109,64 +115,175 @@ Or the `<div>` non progressively enhanced method:
## JavaScript
Include the `plyr.js` script before the closing `</body>` tag and then in your JS create a new instance of Plyr as below.
You can use Plyr as an ES6 module as follows:
```js
import Plyr from 'plyr';
const player = new Plyr('#player');
```
Alternatively you can include the `plyr.js` script before the closing `</body>` tag and then in your JS create a new instance of Plyr as below.
```html
<script src="path/to/plyr.js"></script>
<script>
const player = new Plyr('#player');
const player = new Plyr('#player');
</script>
```
See [initialising](#initialising) for more information on advanced setups.
See [initialising](#initializing) for more information on advanced setups.
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
You can use our CDN (provided by [Cloudflare](https://www.cloudflare.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills separately as part of your application but to make life easier you can use the polyfilled build.
```html
<script src="https://cdn.plyr.io/3.5.2/plyr.js"></script>
<script src="https://cdn.plyr.io/3.7.7/plyr.js"></script>
```
...or...
```html
<script src="https://cdn.plyr.io/3.5.2/plyr.polyfilled.js"></script>
<script src="https://cdn.plyr.io/3.7.7/plyr.polyfilled.js"></script>
```
## CSS
Include the `plyr.css` stylsheet into your `<head>`
Include the `plyr.css` stylesheet into your `<head>`.
```html
<link rel="stylesheet" href="path/to/plyr.css" />
```
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
If you want to use our CDN (provided by [Cloudflare](https://www.cloudflare.com/)) for the default CSS, you can use the following:
```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.5.2/plyr.css" />
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.7/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.5.2/plyr.svg`.
The SVG sprite is loaded automatically from our CDN (provided by [Cloudflare](https://www.cloudflare.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.7.7/plyr.svg`.
### Self hosting
If you don't want to create a build system to include Plyr as an npm module, you can use the pre-built files. You have a few options:
- Download the files from the CDN links above, they're already minified.
- Download the files from [unpkg](https://unpkg.com/browse/plyr/dist/) or similar services.
- Build the project yourself using `npm i && npm run build`, which installs the dependencies and spits out a build to `dist`.
# 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
- [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.
If you do not wish to use Vi, you can set your own `ads.tagUrl` [option](#options).
# Advanced
## SASS
## Customizing the CSS
You can use `bundle.scss` file included in `/src` as part of your build and change variables to suit your design. The SASS require you to
use the [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) plugin (you should be already!) as all declarations use the W3C definitions.
If you want to change any design tokens used for the rendering of the player, you can do so using [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties).
Here's a list of the properties and what they are used for:
| Name | Description | Default / Fallback |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `--plyr-color-main` | The primary UI color. | ![#f03c15](https://place-hold.it/15/00b3ff/000000?text=+) `#00b3ff` |
| `--plyr-video-background` | The background color of video and poster wrappers for using alpha channel videos and poster images. | `rgba(0, 0, 0, 1)` |
| `--plyr-focus-visible-color` | The color used for the focus styles when an element is `:focus-visible` (keyboard focused). | `--plyr-color-main` |
| `--plyr-badge-background` | The background color for badges in the menu. | ![#4a5464](https://place-hold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-badge-text-color` | The text color for badges. | ![#ffffff](https://place-hold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-badge-border-radius` | The border radius used for badges. | `2px` |
| `--plyr-captions-background` | The color for the background of captions. | `rgba(0, 0, 0, 0.8)` |
| `--plyr-captions-text-color` | The color used for the captions text. | ![#ffffff](https://place-hold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-control-icon-size` | The size of the icons used in the controls. | `18px` |
| `--plyr-control-spacing` | The space between controls (sometimes used in a multiple - e.g. `10px / 2 = 5px`). | `10px` |
| `--plyr-control-padding` | The padding inside controls. | `--plyr-control-spacing * 0.7` (`7px`) |
| `--plyr-control-radius` | The border radius used on controls. | `3px` |
| `--plyr-control-toggle-checked-background` | The background color used for checked menu items. | `--plyr-color-main` |
| `--plyr-video-controls-background` | The background for the video controls. | `linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75))` |
| `--plyr-video-control-color` | The text/icon color for video controls. | ![#ffffff](https://place-hold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-video-control-color-hover` | The text/icon color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | ![#ffffff](https://place-hold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-video-control-background-hover` | The background color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | `--plyr-color-main` |
| `--plyr-audio-controls-background` | The background for the audio controls. | ![#ffffff](https://place-hold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-audio-control-color` | The text/icon color for audio controls. | ![#4a5464](https://place-hold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-audio-control-color-hover` | The text/icon color used when audio controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | ![#ffffff](https://place-hold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-audio-control-background-hover` | The background color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | `--plyr-color-main` |
| `--plyr-menu-background` | The background color for menus. | `rgba(255, 255, 255, 0.9)` |
| `--plyr-menu-color` | The text/icon color for menu items. | ![#4a5464](https://place-hold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-menu-shadow` | The shadow used on menus. | `0 1px 2px rgba(0, 0, 0, 0.15)` |
| `--plyr-menu-radius` | The border radius on the menu. | `4px` |
| `--plyr-menu-arrow-size` | The size of the arrow on the bottom of the menu. | `6px` |
| `--plyr-menu-item-arrow-color` | The color of the arrows in the menu. | ![#728197](https://place-hold.it/15/728197/000000?text=+) `#728197` |
| `--plyr-menu-item-arrow-size` | The size of the arrows in the menu. | `4px` |
| `--plyr-menu-border-color` | The border color for the bottom of the back button in the top of the sub menu pages. | ![#dcdfe5](https://place-hold.it/15/dcdfe5/000000?text=+) `#dcdfe5` |
| `--plyr-menu-border-shadow-color` | The shadow below the border of the back button in the top of the sub menu pages. | ![#ffffff](https://place-hold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-progress-loading-size` | The size of the stripes in the loading state in the scrubber. | `25px` |
| `--plyr-progress-loading-background` | The background color on the loading state in the scrubber. | `rgba(35, 40, 47, 0.6)` |
| `--plyr-video-progress-buffered-background` | The fill color for the buffer indication in the scrubber for video. | `rgba(255, 255, 255, 0.25)` |
| `--plyr-audio-progress-buffered-background` | The fill color for the buffer indication in the scrubber for audio. | `rgba(193, 200, 209, 0.6)` |
| `--plyr-range-thumb-height` | The height of the scrubber handle/thumb. | `13px` |
| `--plyr-range-thumb-background` | The background of the scrubber handle/thumb. | ![#ffffff](https://place-hold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-range-thumb-shadow` | The shadow of the scrubber handle/thumb. | `0 1px 1px rgba(215, 26, 18, 0.15), 0 0 0 1px rgba(215, 26, 18, 0.2)` |
| `--plyr-range-thumb-active-shadow-width` | The width of the shadow when the scrubber handle/thumb is `:active` (pressed). | `3px` |
| `--plyr-range-track-height` | The height of the scrubber/progress track. | `5px` |
| `--plyr-range-fill-background` | The fill color of the scrubber/progress. | `--plyr-color-main` |
| `--plyr-video-range-track-background` | The background of the scrubber/progress. | `--plyr-video-progress-buffered-background` |
| `--plyr-video-range-thumb-active-shadow-color` | The color of the shadow when the video scrubber handle/thumb is `:active` (pressed). | `rgba(255, 255, 255, 0.5)` |
| `--plyr-audio-range-track-background` | The background of the scrubber/progress. | `--plyr-video-progress-buffered-background` |
| `--plyr-audio-range-thumb-active-shadow-color` | The color of the shadow when the audio scrubber handle/thumb is `:active` (pressed). | `rgba(215, 26, 18, 0.1)` |
| `--plyr-tooltip-background` | The background color for tooltips. | `rgba(255, 255, 255, 0.9)` |
| `--plyr-tooltip-color` | The text color for tooltips. | ![#4a5464](https://place-hold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-tooltip-padding` | The padding for tooltips. | `calc(var(--plyr-control-spacing) / 2))` |
| `--plyr-tooltip-arrow-size` | The size of the arrow under tooltips. | `4px` |
| `--plyr-tooltip-radius` | The border radius on tooltips. | `3px` |
| `--plyr-tooltip-shadow` | The shadow on tooltips. | `0 1px 2px rgba(0, 0, 0, 0.15)` |
| `--plyr-font-family` | The font family used in the player. | |
| `--plyr-font-size-base` | The base font size. Mainly used for captions. | `15px` |
| `--plyr-font-size-small` | The smaller font size. Mainly used for captions. | `13px` |
| `--plyr-font-size-large` | The larger font size. Mainly used for captions. | `18px` |
| `--plyr-font-size-xlarge` | The even larger font size. Mainly used for captions. | `21px` |
| `--plyr-font-size-time` | The font size for the time. | `--plyr-font-size-small` |
| `--plyr-font-size-menu` | The font size used in the menu. | `--plyr-font-size-small` |
| `--plyr-font-size-badge` | The font size used for badges. | `9px` |
| `--plyr-font-weight-regular` | The regular font weight. | `400` |
| `--plyr-font-weight-bold` | The bold font weight. | `600` |
| `--plyr-line-height` | The line height used within the player. | `1.7` |
| `--plyr-font-smoothing` | Whether to enable font antialiasing within the player. | `false` |
You can set them in your CSS for all players:
```css
:root {
--plyr-color-main: #1ac266;
}
```
...or for a specific class name:
```css
.player {
--plyr-color-main: #1ac266;
}
```
...or in your HTML:
```html
<video class="player" style="--plyr-color-main: #1ac266;">...</video>
```
### Sass
You can use `plyr.scss` file included in `/src/sass` as part of your build and change variables to suit your design. The Sass requires you to
use [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) (you should be already!) as all declarations use the W3C definitions.
The HTML markup uses the BEM methodology with `plyr` as the block, e.g. `.plyr__controls`. You can change the class hooks in the options to match any custom CSS
you write. Check out the JavaScript source for more on this.
@@ -200,31 +317,31 @@ WebVTT captions are supported. To add a caption track, check the HTML example ab
## JavaScript
### Initialising
### Initializing
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 [jQuery](https://jquery.com) object
- A [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)
- A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
- A [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 [multiple players](#multiple-players) below.
#### Single player
Passing a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList):
Passing a CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector):
```javascript
```js
const player = new Plyr('#player');
```
Passing a [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement):
```javascript
```js
const player = new Plyr(document.getElementById('player'));
```
```javascript
```js
const player = new Plyr(document.querySelector('.js-player'));
```
@@ -234,13 +351,13 @@ The HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<
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));
```js
const players = Array.from(document.querySelectorAll('.js-player')).map((p) => new Plyr(p));
```
...or use a static method where you can pass a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of elements, or a [JQuery](https://jquery.com) object:
...or use a static method where you can pass a [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement), or a [JQuery](https://jquery.com) object:
```javascript
```js
const players = Plyr.setup('.js-player');
```
@@ -250,9 +367,9 @@ Both options will also return an array of instances in the order of they were in
The second argument for the constructor is the [options](#options) object:
```javascript
```js
const player = new Plyr('#player', {
title: 'Example Title',
title: 'Example Title',
});
```
@@ -264,47 +381,60 @@ 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` | 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 (`true`/`false`/`'force'`). `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 advertisements. `publisherId`: Your unique [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) publisher ID. |
| `urls` | Object | See source. | If you wish to override any API URLs then you can do so here. You can also set a custom download URL for the download button. |
| `vimeo` | Object | `{ byline: false, portrait: false, title: false, speed: true, transparent: false }` | See [Vimeo embed options](https://github.com/vimeo/player.js/#embed-options). Some are set automatically based on other config options, namely: `loop`, `autoplay`, `muted`, `gesture`, `playsinline` |
| `youtube` | Object | `{ noCookie: false, rel: 0, showinfo: 0, iv_load_policy: 3, modestbranding: 1 }` | See [YouTube embed options](https://developers.google.com/youtube/player_parameters#Parameters). The only custom option is `noCookie` to use an alternative to YouTube that doesn't use cookies (useful for GDPR, etc). Some are set automatically based on other config options, namely: `autoplay`, `hl`, `controls`, `disablekb`, `playsinline`, `cc_load_policy`, `cc_lang_pref`, `widget_referrer` |
| `previewThumbnails` | Object | `{ enabled: false, src: '' }` | `enabled`: Whether to enable the preview thumbnails (they must be generated by you). `src` must be either a string or an array of strings representing URLs for the VTT files containing the image URL(s). Learn more about [preview thumbnails](#preview-thumbnails) below. |
| 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 the default controls are used, you can specify which settings to show in the menu |
| `i18n` | Object | See [defaults.js](/src/js/config/defaults.js) | Used for internationalization (i18n) of the text within the UI. |
| `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`&sup2; | Boolean | `false` | Autoplay the media on load. If the `autoplay` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
| `autopause`&sup1; | Boolean | `true` | Only allow one player playing at once. |
| `playsinline`&sup3; | Boolean | `true` | Allow inline playback on iOS. Note this has no effect on iPadOS. |
| `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 non-selectable language options). |
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false, container: null }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution (`true`/`false`/`'force'`). `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) - note this has no effect on iPadOS. `container`: A selector for an ancestor of the player element, allows contextual content to remain visual in fullscreen mode. Non-ancestors are ignored. |
| `ratio` | String | `null` | Force an aspect ratio for all videos. The format is `'w:h'` - e.g. `'16:9'` or `'4:3'`. If this is not specified then the default for HTML5 and Vimeo is to use the native resolution of the video. As dimensions are not available from YouTube via SDK, 16:9 is forced as a sensible default. |
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4] }` | `selected`: The default speed for playback. `options`: The speed options to display in the UI. YouTube and Vimeo will ignore any options outside of the 0.5-2 range, so options outside of this range will be hidden automatically. |
| `quality` | Object | `{ default: 576, options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240] }` | `default` is the default quality level (if it exists in your sources). `options` are the options to display. This is used to filter the available sources. |
| `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. |
| `ads` | Object | `{ enabled: false, publisherId: '', tagUrl: '' }` | `enabled`: Whether to enable advertisements. `publisherId`: Your unique [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) publisher ID. `tagUrl` is a URL for a custom VAST tag if you're not using Vi. |
| `urls` | Object | See source. | If you wish to override any API URLs then you can do so here. You can also set a custom download URL for the download button. |
| `vimeo` | Object | `{ byline: false, portrait: false, title: false, speed: true, transparent: false }` | See [Vimeo embed options](https://github.com/vimeo/player.js/#embed-options). Some are set automatically based on other config options, namely: `loop`, `autoplay`, `muted`, `gesture`, `playsinline` |
| `youtube` | Object | `{ noCookie: false, rel: 0, showinfo: 0, iv_load_policy: 3, modestbranding: 1 }` | See [YouTube embed options](https://developers.google.com/youtube/player_parameters#Parameters). The only custom option is `noCookie` to use an alternative to YouTube that doesn't use cookies (useful for GDPR, etc). Some are set automatically based on other config options, namely: `autoplay`, `hl`, `controls`, `disablekb`, `playsinline`, `cc_load_policy`, `cc_lang_pref`, `widget_referrer` |
| `previewThumbnails` | Object | `{ enabled: false, src: '' }` | `enabled`: Whether to enable the preview thumbnails (they must be generated by you). `src` must be either a string or an array of strings representing URLs for the VTT files containing the image URL(s). Learn more about [preview thumbnails](#preview-thumbnails) below. |
| `mediaMetadata` | Object | `{ title: '', artist: '', album: '', artwork: [] }` | The [MediaMetadata](https://developer.mozilla.org/en-US/docs/Web/API/MediaMetadata) interface of the Media Session API allows a web page to provide rich media metadata for display in a platform UI. |
| `markers` | Object | `{ enabled: false, points: [] }` | `enabled`: Whether to enable markers. `points` is an array of `{ time: number; label: string; }` objects where `time` represents the marker position in seconds and `label` is the HTML string to be displayed. |
1. Vimeo only
1. Vimeo only
2. Autoplay is generally not recommended as it is seen as a negative user experience. It is also disabled in many browsers. Before raising issues, do your homework. More info can be found here:
- <https://webkit.org/blog/6784/new-video-policies-for-ios/>
- <https://developers.google.com/web/updates/2017/09/autoplay-policy-changes>
- <https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/>
3. YouTube does not support programatically toggling the native fullscreen player via it's API. This means on iOS you have two options, neither being perfect:
- Use the fallback/faux fullscreen option which covers the whole viewport (this is the default)
- Set `playsinline` to `false` and/or `fullscreen.iosNative` to `true` - either option hides the fullscreen toggle in the UI (because of the above API issue) and means iOS will play the video in it's native player.
# API
@@ -314,17 +444,17 @@ There are methods, setters and getters on a Plyr object.
The easiest way to access the Plyr object is to set the return value from your call to the constructor to a variable. For example:
```javascript
```js
const player = new Plyr('#player', {
/* options */
/* options */
});
```
You can also access the object through any events:
```javascript
element.addEventListener('ready', event => {
const player = event.detail.plyr;
```js
element.addEventListener('ready', (event) => {
const player = event.detail.plyr;
});
```
@@ -332,48 +462,49 @@ element.addEventListener('ready', event => {
Example method use:
```javascript
```js
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. |
| `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. |
| Method | Parameters | Description |
| -------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `play()`&sup1; | - | Start playback. |
| `pause()` | - | Pause playback. |
| `togglePlay(toggle)`&sup1; | Boolean | Toggle playback, if no parameters are passed, it will toggle based on current status. |
| `stop()` | - | Stop playback and reset to start. |
| `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. |
| `setPreviewThumbnails(source: PreviewThumbnailsOptions)` | - | Sets the preview thumbnails for the current source. |
| `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) for most browsers - e.g. Chrome, Firefox, Opera, Safari and Edge [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) at time of writing.
## Getters and Setters
Example setters:
```javascript
```js
player.volume = 0.5; // Sets volume at 50%
player.currentTime = 10; // Seeks to 10 seconds
```
Example getters:
```javascript
```js
player.volume; // 0.5;
player.currentTime; // 10
player.fullscreen.active; // false;
@@ -399,15 +530,17 @@ player.fullscreen.active; // false;
| `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. |
| `previewThumbnails` | ✓ | ✓ | Gets or sets the current preview thumbnail source for the player. The setter accepts a string |
| `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`&sup2; | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ (on MacOS Sierra+ and iOS 10+) and Chrome 70+. |
| `pip`&sup1; | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ (on MacOS Sierra+ and iOS 10+) and Chrome 70+. |
| `ratio` | ✓ | ✓ | Gets or sets the video aspect ratio. The setter accepts a string in the same format as the `ratio` option. |
| `download` | ✓ | ✓ | Gets or sets the URL for the download button. The setter accepts a string containing a valid absolute URL. |
1. YouTube only. HTML5 will follow.
2. HTML5 only
1. HTML5 only
### The `.source` setter
@@ -415,101 +548,103 @@ This allows changing the player source and type on the fly.
Video example:
```javascript
```js
player.source = {
type: 'video',
title: 'Example title',
sources: [
{
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',
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: '/path/to/captions.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: '/path/to/captions.fr.vtt',
},
],
type: 'video',
title: 'Example title',
sources: [
{
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',
previewThumbnails: {
src: '/path/to/thumbnails.vtt',
},
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: '/path/to/captions.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: '/path/to/captions.fr.vtt',
},
],
};
```
Audio example:
```javascript
```js
player.source = {
type: 'audio',
title: 'Example title',
sources: [
{
src: '/path/to/audio.mp3',
type: 'audio/mp3',
},
{
src: '/path/to/audio.ogg',
type: 'audio/ogg',
},
],
type: 'audio',
title: 'Example title',
sources: [
{
src: '/path/to/audio.mp3',
type: 'audio/mp3',
},
{
src: '/path/to/audio.ogg',
type: 'audio/ogg',
},
],
};
```
YouTube example:
```javascript
```js
player.source = {
type: 'video',
sources: [
{
src: 'bTqVqk7FSmY',
provider: 'youtube',
},
],
type: 'video',
sources: [
{
src: 'bTqVqk7FSmY',
provider: 'youtube',
},
],
};
```
_Note_: `src` can be the video ID or URL
Vimeo example
```javascript
```js
player.source = {
type: 'video',
sources: [
{
src: '143418951',
provider: 'vimeo',
},
],
type: 'video',
sources: [
{
src: '76979871',
provider: 'vimeo',
},
],
};
```
_Note:_ `src` property for YouTube and Vimeo can either be the video ID or the whole URL.
| Property | Type | Description |
| -------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | String | Either `video` or `audio`. _Note:_ YouTube and Vimeo are currently not supported as audio sources. |
| `title` | String | _Optional._ Title of the new media. Used for the `aria-label` attribute on the play button, and outer container. YouTube and Vimeo are populated automatically. |
| `sources` | Array | This is an array of sources. For HTML5 media, the properties of this object are mapped directly to HTML attributes so more can be added to the object if required. |
| `poster`&sup1; | String | The URL for the poster image (HTML5 video only). |
| `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. |
| Property | Type | Description |
| ------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | String | Either `video` or `audio`. _Note:_ YouTube and Vimeo are currently not supported as audio sources. |
| `title` | String | _Optional._ Title of the new media. Used for the `aria-label` attribute on the play button, and outer container. YouTube and Vimeo are populated automatically. |
| `sources` | Array | This is an array of sources. For HTML5 media, the properties of this object are mapped directly to HTML attributes so more can be added to the object if required. |
| `poster`&sup1; | String | The URL for the poster image (HTML5 video only). |
| `tracks`&sup1; | String | An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above, it will render as `<track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default>` and similar for the French version. Booleans are converted to HTML5 value-less attributes. |
| `previewThumbnails`&sup1; | Object | The same object like in the `previewThumbnails` constructor option. This means you can either change the thumbnails vtt via the `src` key or disable the thumbnails plugin for the next video by passing `{ enabled: false }`. |
1. HTML5 only
1. HTML5 only
# Events
@@ -517,9 +652,9 @@ You can listen for events on the target element you setup Plyr on (see example u
reference to the instance, you can use the `on()` API method or `addEventListener()`. Access to the API can be obtained this way through the `event.detail.plyr`
property. Here's an example:
```javascript
player.on('ready', event => {
const instance = event.detail.plyr;
```js
player.on('ready', (event) => {
const instance = event.detail.plyr;
});
```
@@ -578,8 +713,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.
@@ -616,20 +751,20 @@ Fullscreen in Plyr is supported by all browsers that [currently support it](http
Plyr supports the last 2 versions of most _modern_ browsers.
| Browser | Supported |
| ------------- | ------------- |
| Safari | ✓ |
| Mobile Safari | ✓&sup1; |
| Firefox | ✓ |
| Chrome | ✓ |
| Opera | ✓ |
| Edge | ✓ |
| IE11 | ✓&sup3; |
| IE10 | ✓&sup2;&sup3; |
| Browser | Supported |
| ------------- | --------------- |
| Safari | ✓ |
| Mobile Safari | ✓&sup1; |
| Firefox | ✓ |
| Chrome | ✓ |
| Opera | ✓ |
| Edge | ✓ |
| IE11 | ✓&sup3; |
| IE10 | ✓<sup>2,3</sup> |
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide.
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.
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
@@ -639,23 +774,22 @@ Plyr uses ES6 which isn't supported in all browsers quite yet. This means some f
You can use the static method to check for support. For example
```javascript
const supported = Plyr.supported('video', 'html5', true);
```js
const supported = Plyr.supported('video', 'html5');
```
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' | 'video'`)
- Provider (`'html5' | 'youtube' | 'vimeo'`)
## Disable support programatically
## Disable support programmatically
The `enabled` option can be used to disable certain User Agents. For example, if you don't want to use Plyr for smartphones, you could use:
```javascript
```js
{
enabled: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
enabled: !/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
}
```
@@ -665,14 +799,16 @@ If a User Agent is disabled but supports `<video>` and `<audio>` natively, it wi
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
| Type | Maintainer | Link |
| --------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
| Angular | Simon Bobrov ([@smnbbrv](https://github.com/smnbbrv)) | [https://github.com/smnbbrv/ngx-plyr](https://github.com/smnbbrv/ngx-plyr) |
| 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) |
| Type | Maintainer | Link |
| ----------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
| Angular | Simon Bobrov ([@smnbbrv](https://github.com/smnbbrv)) | [https://github.com/smnbbrv/ngx-plyr](https://github.com/smnbbrv/ngx-plyr) |
| React | Chintan Prajapati ([@chintan9](https://github.com/chintan9)) | [https://github.com/chintan9/plyr-react](https://github.com/chintan9/plyr-react) |
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
| 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) |
| REDAXO | FriendsOfRedaxo / skerbis ([@skerbis](https://friendsofredaxo.github.io)) | [https://github.com/FriendsOfREDAXO/plyr](https://github.com/FriendsOfREDAXO/plyr) |
| svelte-plyr | Ben Woodward / benwoodward ([@benwoodward](https://github.com/benwoodward)) | [https://github.com/benwoodward/svelte-plyr](https://github.com/benwoodward/svelte-plyr) |
# Issues
@@ -687,54 +823,78 @@ Plyr is developed by [@sam_potts](https://twitter.com/sam_potts) / [sampotts.me]
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)
- [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/)
- [Front End Focus #177](https://frontendfoc.us/issues/177)
- [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/)
- [@halfhalftravel](https://www.halfhalftravel.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/)
- [@halfhalftravel](https://www.halfhalftravel.com/)
- [BitChute](https://www.bitchute.com)
- [Rutheneum-Bote](https://gymnasium-rutheneum.de/content/newspaper/kreativwettbewerb.php)
- [pressakey.com | Blog-Magazin für Videospiele](https://pressakey.com)
- [STROLLÿN: Work with a View](https://strollyn.com)
- [CFDA Runway360](https://runway360.cfda.com/)
- [NKLAV | Filmmaker](https://nklav.com)
- [GDI.JS.ORG - Google Drive Index](https://gitlab.com/GoogleDriveIndex/Google-Drive-Index)
Let me know on [Twitter](https://twitter.com/sam_potts) I can add you to the above list. It'd be awesome to see how you're using Plyr :-)
If you want to be added to the list, open a pull request. It'd be awesome to see how you're using Plyr 😎
# Useful links and credits
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 (which Plyr was originally ported from)](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://cdn.plyr.io/static/fastly-logo.png)](https://www.fastly.com/)
- [Cloudflare](https://www.cloudflare.com/) and [Fastly](https://www.fastly.com/) for providing the CDN services.
- [Sentry](https://sentry.io/) for error logging service on the demo website.
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.
## Contributors
[![Sentry](https://cdn.plyr.io/static/sentry-logo-black.svg)](https://sentry.io/)
### Code Contributors
Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site.
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
<a href="https://github.com/sampotts/plyr/graphs/contributors"><img src="https://opencollective.com/plyr/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/plyr/contribute)]
#### Individuals
<a href="https://opencollective.com/plyr"><img src="https://opencollective.com/plyr/individuals.svg?width=890"></a>
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/plyr/contribute)]
<a href="https://opencollective.com/plyr/organization/0/website"><img src="https://opencollective.com/plyr/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/plyr/organization/1/website"><img src="https://opencollective.com/plyr/organization/1/avatar.svg"></a><a href="https://opencollective.com/plyr/organization/2/website"><img src="https://opencollective.com/plyr/organization/2/avatar.svg"></a>
# Copyright and License
[The MIT license](license.md)
[The MIT license](LICENSE.md)
+4 -2
View File
@@ -10,13 +10,15 @@
"src": "./src/js/plyr.polyfilled.js",
"dist": "./dist/",
"formats": ["es", "umd"],
"namespace": "Plyr"
"namespace": "Plyr",
"polyfill": true
},
"demo.js": {
"src": "./demo/src/js/demo.js",
"dist": "./demo/dist/",
"formats": ["iife"],
"namespace": "Demo"
"namespace": "Demo",
"polyfill": true
}
},
"css": {
-1072
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
{
"version": "0.2",
"ignorePaths": ["package.json", "dist/*", "demo/node_modules/*"],
"dictionaryDefinitions": [],
"dictionaries": ["en-gb", "softwareTerms", "html", "css", "typescript"],
"words": [
"autopause",
"autoplay",
"bote",
"cfda",
"classname",
"digbmx",
"fastly",
"fullscreen",
"gordita",
"loadjs",
"magazin",
"menuitemradio",
"noupe",
"otransitionend",
"playsinline",
"plyr",
"rutheneum",
"seektime",
"selz",
"sparkk",
"srclang",
"strol",
"stylelint",
"unmute",
"videospiele",
"xywh"
],
"ignoreWords": [],
"import": []
}
-1
View File
File diff suppressed because one or more lines are too long
-12987
View File
File diff suppressed because it is too large Load Diff
-2
View File
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

-1
View File
File diff suppressed because one or more lines are too long
+232 -250
View File
@@ -1,278 +1,260 @@
<!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" />
<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" />
<!-- 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" />
<!-- Open Graph -->
<meta property="og:title" content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player" />
<meta property="og:site_name" content="Plyr" />
<meta property="og:url" content="https://plyr.io" />
<meta property="og:image" content="https://cdn.plyr.io/static/icons/1200x630.png" />
<!-- Twitter -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@sam_potts" />
<meta name="twitter:creator" content="@sam_potts" />
<meta name="twitter:card" content="summary_large_image" />
<!-- 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" />
<!-- 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>
<!-- 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"
/>
<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
<!-- Google Analytics-->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-132699580-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-132699580-1');
</script>
</head>
<body>
<div class="grid">
<header>
<h1>Pl<span>a</span>y<span>e</span>r</h1>
<p>
A simple, accessible and customisable media player for
<button type="button" class="faux-link" data-source="video">
<svg class="icon">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path></svg
>Video</button
>,
<button type="button" class="faux-link" data-source="audio">
<svg class="icon">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path></svg
>Audio</button
>,
<button type="button" class="faux-link" data-source="youtube">
<svg class="icon" role="presentation">
<title>YouTube</title>
<path
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
M6,11V5l5,3L6,11z"
></path></svg
>YouTube
</button>
and
<button type="button" class="faux-link" data-source="vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
<path
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
></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>
></path></svg
>Vimeo
</button>
</p>
<p>
Premium video monitization from
<a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border">
<img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi" />
<span class="sr-only">ai.vi</span>
</a>
</p>
<div class="call-to-action">
<span class="button--with-count">
<a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr-button">
<svg class="icon" role="presentation">
<title>GitHub</title>
<path
d="M8,0.2c-4.4,0-8,3.6-8,8c0,3.5,2.3,6.5,5.5,7.6
<div class="call-to-action">
<a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr">
<svg class="icon" role="presentation">
<title>GitHub</title>
<path
d="M8,0.2c-4.4,0-8,3.6-8,8c0,3.5,2.3,6.5,5.5,7.6
C5.9,15.9,6,15.6,6,15.4c0-0.2,0-0.7,0-1.4C3.8,14.5,3.3,13,3.3,13c-0.4-0.9-0.9-1.2-0.9-1.2c-0.7-0.5,0.1-0.5,0.1-0.5
c0.8,0.1,1.2,0.8,1.2,0.8C4.4,13.4,5.6,13,6,12.8c0.1-0.5,0.3-0.9,0.5-1.1c-1.8-0.2-3.6-0.9-3.6-4c0-0.9,0.3-1.6,0.8-2.1
c-0.1-0.2-0.4-1,0.1-2.1c0,0,0.7-0.2,2.2,0.8c0.6-0.2,1.3-0.3,2-0.3c0.7,0,1.4,0.1,2,0.3c1.5-1,2.2-0.8,2.2-0.8
c0.4,1.1,0.2,1.9,0.1,2.1c0.5,0.6,0.8,1.3,0.8,2.1c0,3.1-1.9,3.7-3.7,3.9C9.7,12,10,12.5,10,13.2c0,1.1,0,1.9,0,2.2
c0,0.2,0.1,0.5,0.6,0.4c3.2-1.1,5.5-4.1,5.5-7.6C16,3.8,12.4,0.2,8,0.2z"
></path>
</svg>
Download on GitHub
</a>
</span>
</div>
</header>
></path>
</svg>
Download on GitHub
</a>
</div>
</header>
<main>
<div id="container">
<video
controls
crossorigin
playsinline
data-poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg"
id="player"
>
<!-- Video files -->
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4"
type="video/mp4"
size="576"
/>
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4"
type="video/mp4"
size="720"
/>
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4"
type="video/mp4"
size="1080"
/>
<main>
<div id="container">
<video
controls
crossorigin
playsinline
poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg"
id="player"
>
<!-- Video files -->
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4"
type="video/mp4"
size="576"
/>
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4"
type="video/mp4"
size="720"
/>
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4"
type="video/mp4"
size="1080"
/>
<!-- Caption files -->
<track
kind="captions"
label="English"
srclang="en"
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
default
/>
<track
kind="captions"
label="Français"
srclang="fr"
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"
/>
<!-- Caption files -->
<track
kind="captions"
label="English"
srclang="en"
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
default
/>
<track
kind="captions"
label="Français"
srclang="fr"
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"
/>
<!-- Fallback for browsers that don't support the <video> element -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download
>Download</a
>
</video>
</div>
<ul>
<li class="plyr__cite plyr__cite--video" hidden>
<small>
<svg class="icon">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path>
</svg>
<a
href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323"
target="_blank"
>View From A Blue Moon</a
>
&copy; Brainfarm
</small>
</li>
<li class="plyr__cite plyr__cite--audio" hidden>
<small>
<svg class="icon" title="HTML5">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path>
</svg>
<a href="http://www.kishibashi.com/" target="_blank"
>Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;</a
>
&copy; Kishi Bashi
</small>
</li>
<li class="plyr__cite plyr__cite--youtube" hidden>
<small>
<a href="https://www.youtube.com/watch?v=bTqVqk7FSmY" target="_blank"
>View From A Blue Moon</a
>
on&nbsp;
<span class="color--youtube">
<svg class="icon" role="presentation">
<title>YouTube</title>
<path
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
M6,11V5l5,3L6,11z"
></path></svg
>YouTube
</span>
</small>
</li>
<li class="plyr__cite plyr__cite--vimeo" hidden>
<small>
<a href="https://vimeo.com/76979871" target="_blank">The New Vimeo Player</a> on&nbsp;
<span class="color--vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
<path
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
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>
<!-- 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>
<aside>
<svg class="icon">
<title>Twitter</title>
<ul>
<li class="plyr__cite plyr__cite--video" hidden>
<small>
<svg class="icon">
<title>HTML5</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
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path>
</svg>
<a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank"
>View From A Blue Moon</a
>
&copy; Brainfarm
</small>
</li>
<li class="plyr__cite plyr__cite--audio" hidden>
<small>
<svg class="icon" title="HTML5">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path>
</svg>
<a href="http://www.kishibashi.com/" target="_blank"
>Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;</a
>
&copy; Kishi Bashi
</small>
</li>
<li class="plyr__cite plyr__cite--youtube" hidden>
<small>
<a href="https://www.youtube.com/watch?v=bTqVqk7FSmY" target="_blank">View From A Blue Moon</a>
on&nbsp;
<span class="color--youtube">
<svg class="icon" role="presentation">
<title>YouTube</title>
<path
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
M6,11V5l5,3L6,11z"
></path></svg
>YouTube
</span>
</small>
</li>
<li class="plyr__cite plyr__cite--vimeo" hidden>
<small>
<a href="https://vimeo.com/40648169" target="_blank">Toob “Wavaphon” Music Video</a>
on&nbsp;
<span class="color--vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
<path
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"
></path></svg
>Vimeo
</span>
</small>
</li>
</ul>
</main>
</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.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts"
target="_blank"
class="js-shr-button"
>tweet it</a
>
👍
</p>
</aside>
></path>
</svg>
<p>
If you think Plyr's good,
<a
href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts"
target="_blank"
class="js-shr"
>tweet it</a
>
👍
</p>
</aside>
<!-- Polyfills -->
<script
src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL,Math.trunc"
crossorigin="anonymous"
></script>
<!-- Sharing libary (https://shr.one) -->
<script src="https://cdn.shr.one/2.0.0-beta.2/shr.js" crossorigin="anonymous"></script>
<!-- Docs script -->
<script src="dist/demo.js" crossorigin="anonymous"></script>
</body>
<script src="dist/demo.js" crossorigin="anonymous"></script>
</body>
</html>
+14
View File
@@ -0,0 +1,14 @@
{
"name": "plyr-demo",
"version": "1.0.0",
"description": "Demo for Plyr",
"homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>",
"dependencies": {
"@sentry/browser": "^5.22.3",
"core-js": "^3.6.5",
"custom-event-polyfill": "^1.0.7",
"shr-buttons": "2.0.3",
"url-polyfill": "^1.1.10"
}
}
+161 -272
View File
@@ -1,287 +1,176 @@
// ==========================================================================
// Plyr.io demo
// This code is purely for the https://plyr.io website
// Please see readme.md in the root or github.com/sampotts/plyr
// Please see README.md in the root or github.com/sampotts/plyr
// ==========================================================================
import Raven from 'raven-js';
import 'custom-event-polyfill';
import 'url-polyfill';
import * as Sentry from '@sentry/browser';
import Shr from 'shr-buttons';
import Plyr from '../../../src/js/plyr';
import sources from './sources';
(() => {
const { host } = window.location;
const env = {
prod: host === 'plyr.io',
dev: host === 'dev.plyr.io',
};
const production = 'plyr.io';
const isProduction = window.location.host.includes(production);
document.addEventListener('DOMContentLoaded', () => {
Raven.context(() => {
const selector = '#player';
const container = document.getElementById('container');
// Sentry for demo site (https://plyr.io) only
if (isProduction) {
Sentry.init({
dsn: 'https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555',
whitelistUrls: [production].map((d) => new RegExp(`https://(([a-z0-9])+(.))*${d}`)),
});
}
if (window.Shr) {
window.Shr.setup('.js-shr-button', {
count: {
classname: 'button__count',
},
});
}
document.addEventListener('DOMContentLoaded', () => {
const selector = '#player';
// Setup tab focus
const tabClassName = 'tab-focus';
// Remove class on blur
document.addEventListener('focusout', event => {
if (!event.target.classList || container.contains(event.target)) {
return;
}
event.target.classList.remove(tabClassName);
});
// Add classname to tabbed elements
document.addEventListener('keydown', event => {
if (event.keyCode !== 9) {
return;
}
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
const focused = document.activeElement;
if (!focused || !focused.classList || container.contains(focused)) {
return;
}
focused.classList.add(tabClassName);
}, 10);
});
// Setup the player
const player = new Plyr(selector, {
debug: true,
title: 'View From A Blue Moon',
iconUrl: 'dist/demo.svg',
keyboard: {
global: true,
},
tooltips: {
controls: true,
},
captions: {
active: true,
},
keys: {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: {
enabled: env.prod || env.dev,
publisherId: '918848828995742',
},
previewThumbnails: {
enabled: true,
src: [
'https://cdn.plyr.io/static/demo/thumbs/100p.vtt',
'https://cdn.plyr.io/static/demo/thumbs/240p.vtt',
],
},
});
// Expose for tinkering in the console
window.player = player;
// Setup type toggle
const buttons = document.querySelectorAll('[data-source]');
const types = {
video: 'video',
audio: 'audio',
youtube: 'youtube',
vimeo: 'vimeo',
};
let currentType = window.location.hash.replace('#', '');
const historySupport = window.history && window.history.pushState;
// Toggle class on an element
function toggleClass(element, className, state) {
if (element) {
element.classList[state ? 'add' : 'remove'](className);
}
}
// Set a new source
function newSource(type, init) {
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
if (
!(type in types) ||
(!init && type === currentType) ||
(!currentType.length && type === types.video)
) {
return;
}
switch (type) {
case types.video:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
type: 'video/mp4',
size: 576,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
type: 'video/mp4',
size: 720,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
type: 'video/mp4',
size: 1080,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
type: 'video/mp4',
size: 1440,
},
],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
};
break;
case types.audio:
player.source = {
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3',
},
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg',
},
],
};
break;
case types.youtube:
player.source = {
type: 'video',
sources: [
{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
},
],
};
break;
case types.vimeo:
player.source = {
type: 'video',
sources: [
{
src: 'https://vimeo.com/76979871',
provider: 'vimeo',
},
],
};
break;
default:
break;
}
// Set the current type for next time
currentType = type;
// Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
// Set active on parent
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
cite.setAttribute('hidden', '');
});
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
}
// Bind to each button
Array.from(buttons).forEach(button => {
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
newSource(type);
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
}
});
});
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && 'type' in event.state) {
newSource(event.state.type);
}
});
// On load
if (historySupport) {
const video = !currentType.length;
// If there's no current type set, assume video
if (video) {
currentType = types.video;
}
// Replace current history state
if (currentType in types) {
window.history.replaceState(
{
type: currentType,
},
'',
video ? '' : `#${currentType}`,
);
}
// If it's not video, load the source
if (currentType !== types.video) {
newSource(currentType, true);
}
}
});
// Setup share buttons
Shr.setup('.js-shr', {
count: {
className: 'button__count',
},
wrapper: {
className: 'button--with-count',
},
});
// Raven / Sentry
// For demo site (https://plyr.io) only
if (env.prod) {
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
// Setup the player
const player = new Plyr(selector, {
debug: true,
title: 'View From A Blue Moon',
iconUrl: 'dist/demo.svg',
keyboard: {
global: true,
},
tooltips: {
controls: true,
},
captions: {
active: true,
},
/* ads: {
enabled: isProduction,
publisherId: '918848828995742',
}, */
previewThumbnails: {
enabled: true,
src: ['https://cdn.plyr.io/static/demo/thumbs/100p.vtt', 'https://cdn.plyr.io/static/demo/thumbs/240p.vtt'],
},
vimeo: {
// Prevent Vimeo blocking plyr.io demo site
referrerPolicy: 'no-referrer',
},
mediaMetadata: {
title: 'View From A Blue Moon',
album: 'Sports',
artist: 'Brainfarm',
artwork: [
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
type: 'image/jpeg',
},
],
},
markers: {
enabled: true,
points: [
{
time: 10,
label: 'first marker',
},
{
time: 40,
label: 'second marker',
},
{
time: 120,
label: '<strong>third</strong> marker',
},
],
},
});
// Expose for tinkering in the console
window.player = player;
// Setup type toggle
const buttons = document.querySelectorAll('[data-source]');
const types = Object.keys(sources);
const historySupport = Boolean(window.history && window.history.pushState);
let currentType = window.location.hash.substring(1);
const hasInitialType = currentType.length;
function render(type) {
// Remove active classes
Array.from(buttons).forEach((button) => button.parentElement.classList.toggle('active', false));
// Set active on parent
document.querySelector(`[data-source="${type}"]`).classList.toggle('active', true);
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach((cite) => {
// eslint-disable-next-line no-param-reassign
cite.hidden = true;
});
document.querySelector(`.plyr__cite--${type}`).hidden = false;
}
// Set a new source
function setSource(type, init) {
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
if (!types.includes(type) || (!init && type === currentType) || (!currentType.length && type === 'video')) {
return;
}
// Set the new source
player.source = sources[type];
// Set the current type for next time
currentType = type;
render(type);
}
// Bind to each button
Array.from(buttons).forEach((button) => {
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
setSource(type);
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
}
});
});
// List for backwards/forwards
window.addEventListener('popstate', (event) => {
if (event.state && Object.keys(event.state).includes('type')) {
setSource(event.state.type);
}
});
// If there's no current type set, assume video
if (!hasInitialType) {
currentType = 'video';
}
// Replace current history state
if (historySupport && types.includes(currentType)) {
window.history.replaceState({ type: currentType }, '', hasInitialType ? `#${currentType}` : '');
}
// If it's not video, load the source
if (currentType !== 'video') {
setSource(currentType, true);
}
render(currentType);
});
})();
+81
View File
@@ -0,0 +1,81 @@
const sources = {
video: {
type: 'video',
title: 'View From A Blue Moon',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
type: 'video/mp4',
size: 576,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
type: 'video/mp4',
size: 720,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
type: 'video/mp4',
size: 1080,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
type: 'video/mp4',
size: 1440,
},
],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
previewThumbnails: {
src: ['https://cdn.plyr.io/static/demo/thumbs/100p.vtt', 'https://cdn.plyr.io/static/demo/thumbs/240p.vtt'],
},
},
audio: {
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3',
},
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg',
},
],
},
youtube: {
type: 'video',
sources: [
{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
},
],
},
vimeo: {
type: 'video',
sources: [
{
src: 'https://vimeo.com/40648169',
provider: 'vimeo',
},
],
},
};
export default sources;
+6 -1
View File
@@ -1,7 +1,12 @@
@charset "UTF-8";
// ==========================================================================
// Plyr.io Demo Page
// ==========================================================================
@charset 'UTF-8';
@import '../../../../src/sass/lib/css-vars';
$css-vars-use-native: true;
// Settings
@import '../settings/breakpoints';
+2 -1
View File
@@ -1,7 +1,8 @@
@charset "UTF-8";
// ==========================================================================
// Plyr.io Error Page
// ==========================================================================
@charset 'UTF-8';
// Settings
@import '../settings/colors';
+55 -54
View File
@@ -5,79 +5,80 @@
// Shared
.button,
.button__count {
align-items: center;
background: $color-button-background;
border: 0;
border-radius: $border-radius-base;
box-shadow: 0 1px 1px rgba(#000, 0.1);
color: $color-button-text;
display: inline-flex;
padding: ($spacing-base * 0.75);
position: relative;
text-shadow: none;
user-select: none;
vertical-align: middle;
align-items: center;
border: 0;
border-radius: $border-radius-base;
box-shadow: 0 1px 1px rgba(#000, 0.1);
display: inline-flex;
padding: ($spacing-base * 0.75);
position: relative;
text-shadow: none;
user-select: none;
vertical-align: middle;
}
// Buttons
.button {
font-weight: $font-weight-bold;
padding-left: $spacing-base;
padding-right: $spacing-base;
transition: all 0.2s ease;
background: $color-button-background;
color: $color-button-text;
font-weight: $font-weight-bold;
padding-left: ($spacing-base * 1.25);
padding-right: ($spacing-base * 1.25);
transition: all 0.2s ease;
&:hover,
&:focus {
color: $gray-dark;
&:hover,
&:focus {
background: $color-button-background-hover;
// Remove the underline/border
&::after {
display: none;
}
// Remove the underline/border
&::after {
display: none;
}
}
&:hover {
box-shadow: 0 2px 2px rgba(#000, 0.1);
transform: translateY(-1px);
}
&:hover {
box-shadow: 0 2px 2px rgba(#000, 0.1);
}
&:focus {
outline: 0;
}
&:focus {
outline: 0;
}
&.tab-focus {
@include tab-focus();
}
&:focus-visible {
@include focus-visible($color-button-background);
}
&:active {
transform: translateY(1px);
}
&:active {
top: 1px;
}
}
// Button group
.button--with-count {
display: inline-flex;
display: inline-flex;
.button .icon {
flex-shrink: 0;
}
.button .icon {
flex-shrink: 0;
}
}
// Count bubble
.button__count {
animation: fadein 0.2s ease;
margin-left: ($spacing-base / 2);
animation: fade-in 0.2s ease;
background: $color-button-count-background;
color: $color-button-count-text;
margin-left: ($spacing-base * 0.75);
&::before {
border: $arrow-size solid transparent;
border-left-width: 0;
border-right-color: $color-button-background;
content: '';
height: 0;
position: absolute;
right: 100%;
top: 50%;
transform: translateY(-50%);
width: 0;
}
&::before {
border: $arrow-size solid transparent;
border-left-width: 0;
border-right-color: $color-button-count-background;
content: '';
height: 0;
position: absolute;
right: 100%;
top: 50%;
transform: translateY(-50%);
width: 0;
}
}
+21 -10
View File
@@ -3,17 +3,28 @@
// ==========================================================================
header {
padding-bottom: $spacing-base;
text-align: center;
padding-bottom: $spacing-base;
text-align: center;
.call-to-action {
margin-top: ($spacing-base * 1.5);
}
h1 span {
animation: shrink-hide 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) 2s forwards;
display: inline-block;
font-weight: $font-weight-light;
opacity: 0.5;
}
@media only screen and (min-width: $screen-md) {
margin-right: ($spacing-base * 3);
max-width: 360px;
padding-bottom: ($spacing-base * 2);
text-align: left;
.call-to-action {
margin-top: ($spacing-base * 1.5);
}
@media only screen and (min-width: $screen-md) {
margin-right: ($spacing-base * 3);
max-width: 360px;
padding-bottom: ($spacing-base * 2);
text-align: left;
p:first-of-type {
@include font-size($font-size-base + 1);
}
}
}
+8 -6
View File
@@ -2,22 +2,24 @@
// Icons
// ==========================================================================
@use 'sass:math';
// Base size icon styles
.icon {
fill: currentColor;
height: $icon-size;
vertical-align: -3px;
width: $icon-size;
fill: currentColor;
height: $icon-size;
vertical-align: -3px;
width: $icon-size;
}
// Within elements
a svg,
button svg,
label svg {
pointer-events: none;
pointer-events: none;
}
a .icon,
.btn .icon {
margin-right: floor($spacing-base / 3);
margin-right: math.div($spacing-base, 4);
}
+32 -33
View File
@@ -4,46 +4,45 @@
// Make a <button> look like an <a>
button.faux-link {
@extend a; // stylelint-disable-line
@include cancel-button-styles();
@extend a; // stylelint-disable-line
@include cancel-button-styles;
}
// Links
a {
border-bottom: 1px dotted currentColor;
color: $color-link;
font-weight: $font-weight-bold;
position: relative;
text-decoration: none;
transition: all 0.2s ease;
border-bottom: 1px dotted currentColor;
color: $color-link;
position: relative;
text-decoration: none;
transition: all 0.2s ease;
&::after {
background: currentColor;
content: '';
height: 1px;
left: 50%;
position: absolute;
top: 100%;
transform: translateX(-50%);
transition: width 0.2s ease;
width: 0;
}
&:hover,
&:focus {
border-bottom-color: transparent;
outline: 0;
&::after {
background: currentColor;
content: '';
height: 1px;
left: 50%;
position: absolute;
top: 100%;
transform: translateX(-50%);
transition: width 0.2s ease;
width: 0;
width: 100%;
}
}
&:hover,
&:focus {
border-bottom-color: transparent;
outline: 0;
&:focus-visible {
@include focus-visible($color-link);
}
&::after {
width: 100%;
}
}
&.tab-focus {
@include tab-focus();
}
&.no-border::after {
display: none;
}
&.no-border::after {
display: none;
}
}
+3 -3
View File
@@ -5,7 +5,7 @@
// Lists
ul,
li {
list-style: none;
margin: 0;
padding: 0;
list-style: none;
margin: 0;
padding: 0;
}
+2 -2
View File
@@ -5,6 +5,6 @@
img,
video,
audio {
max-width: 100%;
vertical-align: middle;
max-width: 100%;
vertical-align: middle;
}
+3 -3
View File
@@ -3,7 +3,7 @@
// ==========================================================================
nav {
display: flex;
justify-content: center;
margin-bottom: $spacing-base;
display: flex;
justify-content: center;
margin-bottom: $spacing-base;
}
+21 -33
View File
@@ -2,49 +2,37 @@
// Examples
// ==========================================================================
// For non supported browsers
video {
max-width: 100%;
vertical-align: middle;
}
@use 'sass:math';
// Example players
.plyr {
border-radius: $border-radius-base;
box-shadow: 0 2px 5px rgba(#000, 0.2);
margin: $spacing-base auto;
border-radius: $border-radius-large;
box-shadow: 0 2px 15px rgba(#000, 0.1);
margin: $spacing-base auto;
&.plyr--audio {
max-width: 480px;
}
&.plyr--audio {
max-width: 480px;
}
}
.plyr__video-wrapper::after {
border: 1px solid rgba(#000, 0.15);
border-radius: inherit;
bottom: 0;
content: '';
left: 0;
pointer-events: none;
position: absolute;
right: 0;
top: 0;
z-index: 3;
border: 1px solid rgba(#000, 0.15);
border-radius: inherit;
bottom: 0;
content: '';
left: 0;
pointer-events: none;
position: absolute;
right: 0;
top: 0;
z-index: 3;
}
// Style full supported player
.plyr__cite {
display: none;
margin-top: $spacing-base;
color: $color-gray-500;
.icon {
margin-right: ceil($spacing-base / 6);
}
}
.plyr--video:not(.plyr--youtube):not(.plyr--vimeo) ~ ul .plyr__cite--video,
.plyr--audio ~ ul .plyr__cite--audio,
.plyr--youtube ~ ul .plyr__cite--youtube,
.plyr--vimeo ~ ul .plyr__cite--vimeo {
display: block;
.icon {
margin-right: math.div($spacing-base, 6);
}
}
+41 -40
View File
@@ -2,63 +2,64 @@
// Core
// ==========================================================================
@use 'sass:math';
html,
body {
display: flex;
width: 100%;
display: flex;
width: 100%;
}
html {
background: $page-background;
background-attachment: fixed;
height: 100%;
background: $page-background;
background-attachment: fixed;
height: 100%;
}
body {
align-items: center;
display: flex;
flex-direction: column;
min-height: 100%;
align-items: center;
display: flex;
flex-direction: column;
min-height: 100%;
}
.grid {
flex: 1;
overflow: auto;
flex: 1;
overflow: auto;
}
main {
margin: auto;
padding-bottom: 1px; // Collapsing margins
text-align: center;
margin: auto;
padding-bottom: 1px; // Collapsing margins
text-align: center;
}
aside {
align-items: center;
background: #fff;
color: $gray;
display: flex;
flex-shrink: 0;
justify-content: center;
padding: ($spacing-base * 0.75);
position: relative;
text-align: center;
text-shadow: none;
width: 100%;
align-items: center;
background: #fff;
display: flex;
flex-shrink: 0;
justify-content: center;
padding: $spacing-base;
position: relative;
text-align: center;
text-shadow: none;
width: 100%;
.icon {
fill: $color-twitter;
margin-right: ($spacing-base / 2);
}
p {
margin: 0;
}
a {
color: $color-twitter;
&.tab-focus {
@include tab-focus($color-twitter);
}
.icon {
fill: $color-twitter;
margin-right: math.div($spacing-base, 2);
}
p {
margin: 0;
}
a {
color: $color-twitter;
&:focus-visible {
@include focus-visible($color-twitter);
}
}
}
+12 -12
View File
@@ -5,26 +5,26 @@
// Error page
html.error,
.error body {
height: 100%;
height: 100%;
}
html.error {
background: $page-background;
background-attachment: fixed;
background: $page-background;
background-attachment: fixed;
}
.error body {
align-items: center;
display: flex;
width: 100%;
align-items: center;
display: flex;
width: 100%;
}
.error main {
padding: $spacing-base;
text-align: center;
width: 100%;
padding: $spacing-base;
text-align: center;
width: 100%;
p {
@include font-size($font-size-large);
}
p {
@include font-size($font-size-large);
}
}
+10 -10
View File
@@ -3,17 +3,17 @@
// ==========================================================================
.grid {
margin: 0 auto;
padding: $spacing-base;
margin: 0 auto;
padding: $spacing-base;
@media only screen and (min-width: $screen-md) {
align-items: center;
display: flex;
max-width: $container-max-width;
width: 100%;
@media only screen and (min-width: $screen-md) {
align-items: center;
display: flex;
max-width: $container-max-width;
width: 100%;
> * {
flex: 1;
}
> * {
flex: 1;
}
}
}
+24 -7
View File
@@ -3,11 +3,28 @@
// ==========================================================================
// Fade
@keyframes fadein {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes shrink-hide {
0% {
opacity: 0.5;
width: 38px;
}
20% {
width: 45px;
}
100% {
opacity: 0;
width: 0;
}
}
+30 -30
View File
@@ -3,46 +3,46 @@
// ==========================================================================
@font-face {
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-light;
src: url('https://cdn.plyr.io/static/fonts/gordita-light.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-light.woff') format('woff');
font-display: swap;
font-family: Gordita;
font-style: normal;
font-weight: $font-weight-light;
src: url('https://cdn.plyr.io/static/fonts/gordita-light.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-light.woff') format('woff');
}
@font-face {
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-regular;
src: url('https://cdn.plyr.io/static/fonts/gordita-regular.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-regular.woff') format('woff');
font-display: swap;
font-family: Gordita;
font-style: normal;
font-weight: $font-weight-regular;
src: url('https://cdn.plyr.io/static/fonts/gordita-regular.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-regular.woff') format('woff');
}
@font-face {
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-medium;
src: url('https://cdn.plyr.io/static/fonts/gordita-medium.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-medium.woff') format('woff');
font-display: swap;
font-family: Gordita;
font-style: normal;
font-weight: $font-weight-medium;
src: url('https://cdn.plyr.io/static/fonts/gordita-medium.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-medium.woff') format('woff');
}
@font-face {
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-bold;
src: url('https://cdn.plyr.io/static/fonts/gordita-bold.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-bold.woff') format('woff');
font-display: swap;
font-family: Gordita;
font-style: normal;
font-weight: $font-weight-bold;
src: url('https://cdn.plyr.io/static/fonts/gordita-bold.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-bold.woff') format('woff');
}
@font-face {
font-display: swap;
font-family: 'Gordita';
font-style: normal;
font-weight: $font-weight-black;
src: url('https://cdn.plyr.io/static/fonts/gordita-black.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-black.woff') format('woff');
font-display: swap;
font-family: Gordita;
font-style: normal;
font-weight: $font-weight-black;
src: url('https://cdn.plyr.io/static/fonts/gordita-black.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-black.woff') format('woff');
}
+31 -29
View File
@@ -2,53 +2,55 @@
// Mixins
// ==========================================================================
@use 'sass:math';
// Convert a <button> into an <a>
// ---------------------------------------
@mixin cancel-button-styles() {
background: transparent;
border: 0;
border-radius: 0;
cursor: pointer;
font: inherit;
line-height: $line-height-base;
margin: 0;
padding: 0;
position: relative;
text-align: inherit;
text-shadow: inherit;
-moz-user-select: text; // stylelint-disable-line
vertical-align: baseline;
width: auto;
background: transparent;
border: 0;
border-radius: 0;
cursor: pointer;
font: inherit;
line-height: $line-height-base;
margin: 0;
padding: 0;
position: relative;
text-align: inherit;
text-shadow: inherit;
user-select: text;
vertical-align: baseline;
width: auto;
}
// Nicer focus styles
// ---------------------------------------
@mixin tab-focus($color: $tab-focus-default-color) {
box-shadow: 0 0 0 3px rgba($color, 0.35);
outline: 0;
@mixin focus-visible($color: $focus-default-color) {
outline: 2px dashed $color;
outline-offset: 2px;
}
// Use rems for font sizing
// Leave <body> at 100%/16px
// ---------------------------------------
@function calculate-rem($size) {
$rem: $size / 16;
@return #{$rem}rem;
$rem: math.div($size, 16);
@return #{$rem}rem;
}
@mixin font-size($size: 16) {
font-size: $size * 1px; // Fallback in px
font-size: calculate-rem($size);
@mixin font-size($size: $font-size-base) {
font-size: $size * 1px; // Fallback in px
font-size: calculate-rem($size);
}
// Font smoothing
// ---------------------------------------
@mixin font-smoothing($enabled: true) {
@if $enabled {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
} @else {
-moz-osx-font-smoothing: auto;
-webkit-font-smoothing: subpixel-antialiased;
}
@if $enabled {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
} @else {
-moz-osx-font-smoothing: auto;
-webkit-font-smoothing: subpixel-antialiased;
}
}
+73 -74
View File
@@ -10,9 +10,8 @@
*/
html {
line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
line-height: 1.15; /* 1 */
text-size-adjust: 100%; /* 2 */
}
/* Sections
@@ -23,7 +22,7 @@ html {
*/
body {
margin: 0;
margin: 0;
}
/**
@@ -36,7 +35,7 @@ footer,
header,
nav,
section {
display: block;
display: block;
}
/**
@@ -45,8 +44,8 @@ section {
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
@@ -60,8 +59,8 @@ h1 {
figcaption,
figure,
main {
/* 1 */
display: block;
/* 1 */
display: block;
}
/**
@@ -69,7 +68,7 @@ main {
*/
figure {
margin: 1em 40px;
margin: 1em 40px;
}
/**
@@ -78,9 +77,9 @@ figure {
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
@@ -89,8 +88,8 @@ hr {
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
font-family: monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
@@ -102,8 +101,8 @@ pre {
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
background-color: transparent; /* 1 */
text-decoration-skip: objects; /* 2 */
}
/**
@@ -112,9 +111,9 @@ a {
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
border-bottom: 0; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
@@ -123,7 +122,7 @@ abbr[title] {
b,
strong {
font-weight: inherit;
font-weight: inherit;
}
/**
@@ -132,7 +131,7 @@ strong {
b,
strong {
font-weight: bolder;
font-weight: bolder;
}
/**
@@ -143,8 +142,8 @@ strong {
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
@@ -152,7 +151,7 @@ samp {
*/
dfn {
font-style: italic;
font-style: italic;
}
/**
@@ -160,8 +159,8 @@ dfn {
*/
mark {
background-color: #ff0;
color: #000;
background-color: #ff0;
color: #000;
}
/**
@@ -169,7 +168,7 @@ mark {
*/
small {
font-size: 80%;
font-size: 80%;
}
/**
@@ -179,18 +178,18 @@ small {
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
bottom: -0.25em;
}
sup {
top: -0.5em;
top: -0.5em;
}
/* Embedded content
@@ -202,7 +201,7 @@ sup {
audio,
video {
display: inline-block;
display: inline-block;
}
/**
@@ -210,8 +209,8 @@ video {
*/
audio:not([controls]) {
display: none;
height: 0;
display: none;
height: 0;
}
/**
@@ -219,7 +218,7 @@ audio:not([controls]) {
*/
img {
border-style: none;
border-style: none;
}
/**
@@ -227,7 +226,7 @@ img {
*/
svg:not(:root) {
overflow: hidden;
overflow: hidden;
}
/* Forms
@@ -243,10 +242,10 @@ input,
optgroup,
select,
textarea {
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
@@ -256,8 +255,8 @@ textarea {
button,
input {
/* 1 */
overflow: visible;
/* 1 */
overflow: visible;
}
/**
@@ -267,8 +266,8 @@ input {
button,
select {
/* 1 */
text-transform: none;
/* 1 */
text-transform: none;
}
/**
@@ -281,7 +280,7 @@ button,
html [type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button; /* 2 */
appearance: button; /* 2 */
}
/**
@@ -292,8 +291,8 @@ button::-moz-focus-inner,
[type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner {
border-style: none;
padding: 0;
border-style: none;
padding: 0;
}
/**
@@ -304,7 +303,7 @@ button:-moz-focusring,
[type='button']:-moz-focusring,
[type='reset']:-moz-focusring,
[type='submit']:-moz-focusring {
outline: 1px dotted ButtonText;
outline: 1px dotted ButtonText;
}
/**
@@ -312,7 +311,7 @@ button:-moz-focusring,
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
padding: 0.35em 0.75em 0.625em;
}
/**
@@ -323,12 +322,12 @@ fieldset {
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
@@ -337,8 +336,8 @@ legend {
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
@@ -346,7 +345,7 @@ progress {
*/
textarea {
overflow: auto;
overflow: auto;
}
/**
@@ -356,8 +355,8 @@ textarea {
[type='checkbox'],
[type='radio'] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
@@ -366,7 +365,7 @@ textarea {
[type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button {
height: auto;
height: auto;
}
/**
@@ -375,8 +374,8 @@ textarea {
*/
[type='search'] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
@@ -385,7 +384,7 @@ textarea {
[type='search']::-webkit-search-cancel-button,
[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
appearance: none;
}
/**
@@ -394,8 +393,8 @@ textarea {
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
@@ -408,7 +407,7 @@ textarea {
details,
menu {
display: block;
display: block;
}
/*
@@ -416,7 +415,7 @@ menu {
*/
summary {
display: list-item;
display: list-item;
}
/* Scripting
@@ -427,7 +426,7 @@ summary {
*/
canvas {
display: inline-block;
display: inline-block;
}
/**
@@ -435,7 +434,7 @@ canvas {
*/
template {
display: none;
display: none;
}
/* Hidden
@@ -446,5 +445,5 @@ template {
*/
[hidden] {
display: none;
display: none;
}
+1 -1
View File
@@ -7,5 +7,5 @@
*,
*::after,
*::before {
box-sizing: border-box;
box-sizing: border-box;
}
+27 -17
View File
@@ -2,31 +2,41 @@
// Colors
// ==========================================================================
// Greyscale
$gray-dark: #343f4a;
$gray: #55646b;
$gray-light: #cbd0d3;
$gray-lighter: #dbe3e8;
$off-white: #f2f5f7;
// Grayscale
$color-gray-900: hsl(210deg 15% 16%);
$color-gray-800: lighten($color-gray-900, 9%);
$color-gray-700: lighten($color-gray-800, 9%);
$color-gray-600: lighten($color-gray-700, 9%);
$color-gray-500: lighten($color-gray-600, 9%);
$color-gray-400: lighten($color-gray-500, 9%);
$color-gray-300: lighten($color-gray-400, 9%);
$color-gray-200: lighten($color-gray-300, 9%);
$color-gray-100: lighten($color-gray-200, 9%);
$color-gray-50: lighten($color-gray-100, 9%);
// Branding
$color-brand-primary: hsl(198deg 100% 50%);
// Text
$color-text: #fff;
// Plyr
$color-brand-primary: #1aafff;
$color-text: $color-gray-700;
$color-headings: $color-brand-primary;
// Brands
$color-twitter: #4baaf4;
$color-youtube: #cc181e;
$color-vimeo: #19b7ed;
// Elements
$color-link: #fff;
$color-background: $color-brand-primary;
$color-link: $color-brand-primary;
// Background
$color-background-from: hsl(198deg 100% 94%);
$color-background-to: hsl(198deg 100% 98%);
// Buttons
$color-button-background: #fff;
$color-button-text: $gray;
$color-button-background: $color-brand-primary;
$color-button-text: #fff;
$color-button-background-hover: hsl(198deg 100% 55%);
$color-button-count-background: #fff;
$color-button-count-text: $color-gray-600;
// Focus
$tab-focus-default-color: #fff;
$focus-default-color: $color-brand-primary;
+2 -1
View File
@@ -7,6 +7,7 @@ $arrow-size: 5px;
// Radii
$border-radius-base: 4px;
$border-radius-large: 8px;
// Background
$page-background: linear-gradient(to left top, lighten($color-background, 10%), darken($color-background, 20%));
$page-background: linear-gradient(to left top, $color-background-from, $color-background-to);
+1 -1
View File
@@ -2,4 +2,4 @@
// Layout
// ==========================================================================
$container-max-width: 1260px;
$container-max-width: 1240px;
+14 -18
View File
@@ -2,21 +2,17 @@
// Plyr Settings
// ==========================================================================
// Font
$plyr-font-family: inherit;
// Sizes
$plyr-font-size-base: 13px;
$plyr-font-size-small: 12px;
$plyr-font-size-time: 11px;
$plyr-font-size-badges: 9px;
// Other
$plyr-font-smoothing: true;
// Captions
$plyr-font-size-captions-base: $plyr-font-size-base;
$plyr-font-size-captions-small: $plyr-font-size-small;
$plyr-font-size-captions-medium: 18px;
$plyr-font-size-captions-large: 21px;
$plyr-font-size-menu: $plyr-font-size-base;
@include css-vars(
(
--plyr-color-main: $color-brand-primary,
--plyr-font-size-base: 13px,
--plyr-font-size-small: 12px,
--plyr-font-size-time: 11px,
--plyr-font-size-badges: 9px,
--plyr-font-size-menu: var(--plyr-font-size-base),
--plyr-font-weight-regular: 500,
--plyr-font-weight-bold: 600,
--plyr-font-size-captions-medium: 18px,
--plyr-font-size-captions-large: 21px,
)
);
+1 -1
View File
@@ -2,4 +2,4 @@
// Colors
// ==========================================================================
$spacing-base: 20px;
$spacing-base: 16px;
+1 -5
View File
@@ -3,19 +3,15 @@
// ==========================================================================
$font-sans-serif: 'Gordita', 'Avenir', 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol';
'Segoe UI Symbol';
$font-size-base: 15;
$font-size-small: 13;
$font-size-large: 18;
$font-size-h1: 64;
$font-weight-light: 300;
$font-weight-regular: 400;
$font-weight-medium: 500;
$font-weight-bold: 600;
$font-weight-black: 900;
$line-height-base: 1.75;
$letter-spacing-headings: -0.025em;
+13 -12
View File
@@ -4,32 +4,33 @@
// Set to 100% for rem sizing
html {
font-size: 100%;
font-size: 100%;
}
body {
@include font-smoothing();
@include font-size($font-size-base);
color: $color-text;
font-family: $font-sans-serif;
font-weight: $font-weight-medium;
line-height: $line-height-base;
text-shadow: 0 1px 1px rgba(#000, 0.15);
@include font-smoothing;
@include font-size($font-size-base);
color: $color-text;
font-family: $font-sans-serif;
font-weight: $font-weight-medium;
line-height: $line-height-base;
}
button,
input,
select,
textarea {
font: inherit;
font: inherit;
}
p,
small {
margin: 0 0 $spacing-base;
margin: 0 0 ($spacing-base * 1.5);
}
small {
@include font-size($font-size-small);
display: block;
@include font-size($font-size-small);
display: block;
}
+7 -5
View File
@@ -3,9 +3,11 @@
// ==========================================================================
h1 {
@include font-size($font-size-h1);
font-weight: $font-weight-bold;
letter-spacing: $letter-spacing-headings;
line-height: 1.2;
margin: 0 0 $spacing-base;
@include font-size($font-size-h1);
color: $color-headings;
font-weight: $font-weight-bold;
letter-spacing: $letter-spacing-headings;
line-height: 1.2;
margin: 0 0 ($spacing-base * 1.5);
}
+1 -1
View File
@@ -3,5 +3,5 @@
// ==========================================================================
.no-border {
border: 0;
border: 0;
}
+4
View File
@@ -0,0 +1,4 @@
*:focus-visible {
outline: 2px dotted $color-brand-primary;
outline-offset: 2px;
}
+10 -10
View File
@@ -3,18 +3,18 @@
// ==========================================================================
[hidden] {
display: none;
display: none;
}
// Hide only visually, but have it available for screen readers: h5bp.com/v
.sr-only {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
opacity: 0.001;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
opacity: 0.001;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
+80
View File
@@ -0,0 +1,80 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@sentry/browser@^5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.22.3.tgz#7a64bd1cf01bf393741a3e4bf35f82aa927f5b4e"
integrity sha512-2TzE/CoBa5ZkvxJizDdi1Iz1ldmXSJpFQ1mL07PIXBjCt0Wxf+WOuFSj5IP4L40XHfJE5gU8wEvSH0VDR8nXtA==
dependencies:
"@sentry/core" "5.22.3"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/core@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.22.3.tgz#030f435f2b518f282ba8bd954dac90cd70888bd7"
integrity sha512-eGL5uUarw3o4i9QUb9JoFHnhriPpWCaqeaIBB06HUpdcvhrjoowcKZj1+WPec5lFg5XusE35vez7z/FPzmJUDw==
dependencies:
"@sentry/hub" "5.22.3"
"@sentry/minimal" "5.22.3"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/hub@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.22.3.tgz#08309a70d2ea8d5e313d05840c1711f34f2fffe5"
integrity sha512-INo47m6N5HFEs/7GMP9cqxOIt7rmRxdERunA3H2L37owjcr77MwHVeeJ9yawRS6FMtbWXplgWTyTIWIYOuqVbw==
dependencies:
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/minimal@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.22.3.tgz#706e4029ae5494123d3875c658ba8911aa5cc440"
integrity sha512-HoINpYnVYCpNjn2XIPIlqH5o4BAITpTljXjtAftOx6Hzj+Opjg8tR8PWliyKDvkXPpc4kXK9D6TpEDw8MO0wZA==
dependencies:
"@sentry/hub" "5.22.3"
"@sentry/types" "5.22.3"
tslib "^1.9.3"
"@sentry/types@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.22.3.tgz#d1d547b30ee8bd7771fa893af74c4f3d71f0fd18"
integrity sha512-cv+VWK0YFgCVDvD1/HrrBWOWYG3MLuCUJRBTkV/Opdy7nkdNjhCAJQrEyMM9zX0sac8FKWKOHT0sykNh8KgmYw==
"@sentry/utils@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.22.3.tgz#e3bda3e789239eb16d436f768daa12829f33d18f"
integrity sha512-AHNryXMBvIkIE+GQxTlmhBXD0Ksh+5w1SwM5qi6AttH+1qjWLvV6WB4+4pvVvEoS8t5F+WaVUZPQLmCCWp6zKw==
dependencies:
"@sentry/types" "5.22.3"
tslib "^1.9.3"
core-js@^3.6.5:
version "3.6.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
custom-event-polyfill@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee"
integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
shr-buttons@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/shr-buttons/-/shr-buttons-2.0.3.tgz#2ffd021fc3d789e1510ce2736b938bd09ea1da5a"
integrity sha512-sPAgHiw4uaIt9TnxTfyZEedDChcldSVtnBHE44cpe/mSC7rqm4IEKZRLYqnVlTcGM+FSDNBPUNpSf50Q2ntd+w==
tslib@^1.9.3:
version "1.11.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
url-polyfill@^1.1.10:
version "1.1.10"
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.10.tgz#fd5bbcf66c9491fa682b0cb6d6a855e1643a9281"
integrity sha512-vSaPpaRgBrf41+Uky1myiSh6gpcbw8FpwHYnEy0abxndojOBnIs+yh/49gKYFLtUMP9qoNWjn6j9aUVy23Ie2A==
+10 -10
View File
@@ -1,12 +1,12 @@
{
"cdn": {
"bucket": "plyr",
"domain": "cdn.plyr.io",
"region": "us-east-1"
},
"demo": {
"bucket": "plyr.io",
"domain": "plyr.io",
"region": "us-west-1"
}
"cdn": {
"bucket": "plyr",
"domain": "cdn.plyr.io",
"region": "us-east-1"
},
"demo": {
"bucket": "plyr.io",
"domain": "plyr.io",
"region": "us-west-1"
}
}
BIN
View File
Binary file not shown.
-1
View File
File diff suppressed because one or more lines are too long
-8905
View File
File diff suppressed because it is too large Load Diff
-2
View File
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
-8897
View File
File diff suppressed because it is too large Load Diff
-12309
View File
File diff suppressed because it is too large Load Diff
-2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-12301
View File
File diff suppressed because it is too large Load Diff
-1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

+2 -475
View File
@@ -1,481 +1,8 @@
// ==========================================================================
// Gulp build script
// ==========================================================================
/* global require, __dirname */
/* eslint no-console: "off" */
const path = require('path');
const gulp = require('gulp');
const HubRegistry = require('gulp-hub');
// JavaScript
const terser = require('gulp-terser');
const rollup = require('gulp-better-rollup');
const babel = require('rollup-plugin-babel');
const commonjs = require('rollup-plugin-commonjs');
const resolve = require('rollup-plugin-node-resolve');
// CSS
const sass = require('gulp-sass');
const clean = require('gulp-clean-css');
const prefix = require('gulp-autoprefixer');
// Images
const svgstore = require('gulp-svgstore');
const imagemin = require('gulp-imagemin');
// Utils
const del = require('del');
const filter = require('gulp-filter');
const header = require('gulp-header');
const gitbranch = require('git-branch');
const rename = require('gulp-rename');
const replace = require('gulp-replace');
const ansi = require('ansi-colors');
const log = require('fancy-log');
const open = require('gulp-open');
const plumber = require('gulp-plumber');
const size = require('gulp-size');
const sourcemaps = require('gulp-sourcemaps');
const through = require('through2');
// Deployment
const aws = require('aws-sdk');
const publish = require('gulp-awspublish');
const FastlyPurge = require('fastly-purge');
const pkg = require('./package.json');
const build = require('./build.json');
const deploy = require('./deploy.json');
const { browserslist: browsers, version } = pkg;
const minSuffix = '.min';
// Get AWS config
Object.values(deploy).forEach(target => {
Object.assign(target, {
publisher: publish.create({
region: target.region,
params: {
Bucket: target.bucket,
},
credentials: new aws.SharedIniFileCredentials({ profile: 'plyr' }),
}),
});
});
// Paths
const paths = {
plyr: {
// Source paths
src: {
sass: path.join(__dirname, 'src/sass/**/*.scss'),
js: path.join(__dirname, 'src/js/**/*.js'),
sprite: path.join(__dirname, 'src/sprite/*.svg'),
},
// Output paths
output: path.join(__dirname, 'dist/'),
},
demo: {
// Source paths
src: {
sass: path.join(__dirname, 'demo/src/sass/**/*.scss'),
js: path.join(__dirname, 'demo/src/js/**/*.js'),
},
// Output paths
output: path.join(__dirname, 'demo/dist/'),
// Demo
root: path.join(__dirname, 'demo/'),
},
upload: [
path.join(__dirname, `dist/*${minSuffix}.*`),
path.join(__dirname, 'dist/*.css'),
path.join(__dirname, 'dist/*.svg'),
path.join(__dirname, `demo/dist/*${minSuffix}.*`),
path.join(__dirname, 'demo/dist/*.css'),
path.join(__dirname, 'demo/dist/*.svg'),
],
};
// Task arrays
const tasks = {
css: [],
js: [],
sprite: [],
clean: 'clean',
};
// Size plugin
const sizeOptions = { showFiles: true, gzip: true };
// Clean out /dist
gulp.task(tasks.clean, done => {
const dirs = [paths.plyr.output, paths.demo.output].map(dir => path.join(dir, '**/*'));
// Don't delete the mp4
dirs.push(`!${path.join(paths.plyr.output, '**/*.mp4')}`);
del(dirs);
done();
});
// JavaScript
Object.entries(build.js).forEach(([filename, entry]) => {
entry.formats.forEach(format => {
const name = `js:${filename}:${format}`;
tasks.js.push(name);
const polyfill = filename.includes('polyfilled');
const extension = format === 'es' ? 'mjs' : 'js';
gulp.task(name, () =>
gulp
.src(entry.src)
.pipe(plumber())
.pipe(sourcemaps.init())
.pipe(
rollup(
{
plugins: [
resolve(),
commonjs(),
babel({
presets: [
[
'@babel/env',
{
// debug: true,
useBuiltIns: polyfill ? 'usage' : false,
},
],
],
babelrc: false,
exclude: [/\/core-js\//],
}),
],
},
{
name: entry.namespace,
format,
},
),
)
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
.pipe(
rename({
extname: `.${extension}`,
}),
)
.pipe(gulp.dest(entry.dist))
.pipe(filter(`**/*.${extension}`))
.pipe(terser())
.pipe(rename({ suffix: minSuffix }))
.pipe(size(sizeOptions))
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(entry.dist)),
);
});
});
// CSS
Object.entries(build.css).forEach(([filename, entry]) => {
const name = `css:${filename}`;
tasks.css.push(name);
gulp.task(name, () =>
gulp
.src(entry.src)
.pipe(plumber())
.pipe(sass())
.pipe(
prefix(browsers, {
cascade: false,
}),
)
.pipe(clean())
.pipe(size(sizeOptions))
.pipe(gulp.dest(entry.dist)),
);
});
// SVG Sprites
Object.entries(build.sprite).forEach(([filename, entry]) => {
const name = `sprite:${filename}`;
tasks.sprite.push(name);
gulp.task(name, () =>
gulp
.src(entry.src)
.pipe(plumber())
.pipe(imagemin())
.pipe(svgstore())
.pipe(rename({ basename: path.parse(filename).name }))
.pipe(size(sizeOptions))
.pipe(gulp.dest(entry.dist)),
);
});
// Build all JS
gulp.task('js', () => gulp.parallel(...tasks.js));
// Watch for file changes
gulp.task('watch', () => {
// Plyr core
gulp.watch(paths.plyr.src.js, gulp.parallel(...tasks.js));
gulp.watch(paths.plyr.src.sass, gulp.parallel(...tasks.css));
gulp.watch(paths.plyr.src.sprite, gulp.parallel(...tasks.sprite));
// Demo
gulp.watch(paths.demo.src.js, gulp.parallel(...tasks.js));
gulp.watch(paths.demo.src.sass, gulp.parallel(...tasks.css));
});
// Build distribution
gulp.task('build', gulp.series(tasks.clean, gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite)));
// Default gulp task
gulp.task('default', gulp.series('build', 'watch'));
// Publish a version to CDN and demo
// --------------------------------------------
// Get deployment config
let credentials = {};
try {
credentials = require('./credentials.json'); //eslint-disable-line
} catch (e) {
// Do nothing
}
// Get branch info
const branch = {
current: gitbranch.sync(),
master: 'master',
beta: 'beta',
};
const maxAge = 31536000; // 1 year
const options = {
cdn: {
headers: {
'Cache-Control': `max-age=${maxAge}`,
},
},
demo: {
uploadPath: branch.current === branch.beta ? 'beta' : null,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
},
},
symlinks(ver, filename) {
return {
headers: {
// http://stackoverflow.com/questions/2272835/amazon-s3-object-redirect
'x-amz-website-redirect-location': `/${ver}/${filename}`,
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
},
};
},
};
const regex =
'(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*).(?:0|[1-9][0-9]*)(?:-[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?(?:\\+[\\da-z\\-]+(?:.[\\da-z\\-]+)*)?';
const semver = new RegExp(`v${regex}`, 'gi');
const localPath = new RegExp('(../)?dist', 'gi');
const versionPath = `https://${deploy.cdn.domain}/${version}`;
const cdnpath = new RegExp(`${deploy.cdn.domain}/${regex}/`, 'gi');
const renameFile = rename(p => {
p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
p.dirname = p.dirname.replace('.', version); // eslint-disable-line
});
// Check we're on the correct branch to deploy
const canDeploy = () => {
const allowed = [branch.master, branch.beta];
if (!allowed.includes(branch.current)) {
console.error(`Must be on ${allowed.join(', ')} to publish! (current: ${branch.current})`);
return false;
}
return true;
};
gulp.task('version', done => {
if (!canDeploy()) {
done();
return null;
}
const { domain } = deploy.cdn;
log(`Uploading ${ansi.green.bold(version)} to ${ansi.cyan(domain)}...`);
// Replace versioned URLs in source
const files = ['plyr.js', 'plyr.polyfilled.js', 'config/defaults.js'];
return gulp
.src(files.map(file => path.join(__dirname, `src/js/${file}`)), { base: '.' })
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${domain}/${version}/`))
.pipe(gulp.dest('./'));
});
// Publish version to CDN bucket
gulp.task('cdn', done => {
if (!canDeploy()) {
done();
return null;
}
const { domain, publisher } = deploy.cdn;
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
log(`Uploading ${ansi.green.bold(pkg.version)} to ${ansi.cyan(domain)}...`);
// Upload to CDN
return (
gulp
.src(paths.upload)
.pipe(renameFile)
// Remove min suffix from source map URL
.pipe(
replace(
/sourceMappingURL=([\w-?.]+)/,
(match, filename) => `sourceMappingURL=${filename.replace(minSuffix, '')}`,
),
)
.pipe(size(sizeOptions))
.pipe(replace(localPath, versionPath))
.pipe(publisher.publish(options.cdn.headers))
.pipe(publish.reporter())
);
});
// Purge the fastly cache incase any 403/404 are cached
gulp.task('purge', () => {
if (!Object.keys(credentials).includes('fastly')) {
throw new Error('Fastly credentials required to purge cache.');
}
const { fastly } = credentials;
const list = [];
return gulp
.src(paths.upload)
.pipe(
through.obj((file, enc, cb) => {
const filename = file.path.split('/').pop();
list.push(`${versionPath}/${filename.replace(minSuffix, '')}`);
cb(null);
}),
)
.on('end', () => {
const purge = new FastlyPurge(fastly.token);
list.forEach(url => {
log(`Purging ${ansi.cyan(url)}...`);
purge.url(url, (error, result) => {
if (error) {
log.error(error);
} else if (result) {
log(result);
}
});
});
});
});
// Publish to demo bucket
gulp.task('demo', done => {
if (!canDeploy()) {
done();
return null;
}
const { publisher } = deploy.demo;
const { domain } = deploy.cdn;
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
log(`Uploading ${ansi.green.bold(pkg.version)} to ${ansi.cyan(domain)}...`);
// Replace versioned files in readme.md
gulp.src([`${__dirname}/readme.md`])
.pipe(replace(cdnpath, `${domain}/${version}/`))
.pipe(gulp.dest(__dirname));
// Replace local file paths with remote paths in demo HTML
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
const index = `${paths.demo.root}index.html`;
const error = `${paths.demo.root}error.html`;
const pages = [index];
if (branch.current === branch.master) {
pages.push(error);
}
return gulp
.src(pages)
.pipe(replace(localPath, versionPath))
.pipe(publisher.publish(options.demo.headers))
.pipe(publish.reporter());
});
gulp.task('error', done => {
// Only update CDN for master (prod)
if (!canDeploy() || branch.current !== branch.master) {
done();
return null;
}
const { publisher } = deploy.cdn;
if (!publisher) {
throw new Error('No publisher instance. Check AWS configuration.');
}
// Replace local file paths with remote paths in demo HTML
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
// Upload error.html to cdn
return gulp
.src(`${paths.demo.root}error.html`)
.pipe(replace(localPath, versionPath))
.pipe(publisher.publish(options.demo.headers))
.pipe(publish.reporter());
});
// Open the demo site to check it's ok
gulp.task('open', () => {
const { domain } = deploy.demo;
return gulp.src(__filename).pipe(
open({
uri: `https://${domain}/${branch.current === branch.beta ? 'beta' : ''}`,
}),
);
});
// Do everything
gulp.task(
'deploy',
gulp.series(
'version',
tasks.clean,
gulp.parallel(...tasks.js, ...tasks.css, ...tasks.sprite),
'cdn',
'demo',
'purge',
'open',
),
);
gulp.registry(new HubRegistry(['tasks/*.js']));
+99 -92
View File
@@ -1,94 +1,101 @@
{
"name": "plyr",
"version": "3.5.2",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>",
"main": "dist/plyr.js",
"module": "dist/plyr.min.mjs",
"jsnext:main": "dist/plyr.min.mjs",
"browser": "dist/plyr.min.js",
"sass": "src/sass/plyr.scss",
"style": "dist/plyr.css",
"keywords": [
"HTML5 Video",
"HTML5 Audio",
"Media Player",
"DASH",
"Shaka",
"WordPress",
"HLS"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "git://github.com/sampotts/plyr.git"
},
"bugs": {
"url": "https://github.com/sampotts/plyr/issues"
},
"browserslist": "> 1%",
"scripts": {
"build": "gulp build",
"lint": "eslint src/js && npm run-script remark",
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
"deploy": "yarn lint && gulp deploy"
},
"devDependencies": {
"ansi-colors": "^3.2.3",
"aws-sdk": "^2.409.0",
"@babel/core": "^7.3.3",
"@babel/preset-env": "^7.3.1",
"babel-eslint": "^10.0.1",
"del": "^3.0.0",
"eslint": "^5.14.1",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^4.0.0",
"eslint-plugin-import": "^2.16.0",
"fancy-log": "^1.3.3",
"fastly-purge": "^1.0.1",
"git-branch": "^2.0.1",
"gulp": "^4.0.0",
"gulp-autoprefixer": "^6.0.0",
"gulp-awspublish": "^4.0.0",
"gulp-better-rollup": "^3.4.0",
"gulp-clean-css": "^4.0.0",
"gulp-filter": "^5.1.0",
"gulp-header": "^2.0.7",
"gulp-imagemin": "^5.0.3",
"gulp-open": "^3.0.1",
"gulp-plumber": "^1.2.1",
"gulp-postcss": "^8.0.0",
"gulp-rename": "^1.4.0",
"gulp-replace": "^1.0.0",
"gulp-sass": "^4.0.2",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.5",
"gulp-svgstore": "^7.0.1",
"gulp-terser": "^1.1.7",
"postcss-custom-properties": "^8.0.9",
"prettier-eslint": "^8.8.2",
"prettier-stylelint": "^0.4.2",
"remark-cli": "^6.0.1",
"remark-validate-links": "^8.0.0",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-commonjs": "^9.2.1",
"rollup-plugin-node-resolve": "^4.0.1",
"stylelint": "^9.10.1",
"stylelint-config-prettier": "^5.0.0",
"stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.3.0",
"stylelint-order": "^2.0.0",
"stylelint-scss": "^3.5.4",
"stylelint-selector-bem-pattern": "^2.0.0",
"through2": "^3.0.0"
},
"dependencies": {
"core-js": "^2.6.5",
"custom-event-polyfill": "^1.0.6",
"loadjs": "^3.5.5",
"rangetouch": "^2.0.0",
"raven-js": "^3.27.0",
"url-polyfill": "^1.1.3"
}
"name": "plyr",
"version": "3.7.7",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>",
"main": "dist/plyr.js",
"types": "src/js/plyr.d.ts",
"module": "dist/plyr.min.mjs",
"jsnext:main": "dist/plyr.min.mjs",
"browser": "dist/plyr.min.js",
"sass": "src/sass/plyr.scss",
"style": "dist/plyr.css",
"keywords": [
"HTML5 Video",
"HTML5 Audio",
"Media Player",
"DASH",
"Shaka",
"WordPress",
"HLS"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "git://github.com/sampotts/plyr.git"
},
"bugs": {
"url": "https://github.com/sampotts/plyr/issues"
},
"browserslist": "> 1%",
"scripts": {
"build": "gulp build",
"lint": "eslint src/js && npm run remark && stylelint **/*.scss",
"lint:fix": "eslint --fix src/js && stylelint **/*.scss --fix",
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
"deploy": "npm run lint && gulp version && gulp build && gulp deploy",
"format": "prettier --write \"./{src,demo/src}/**/*.{js,scss}\"",
"spellcheck": "cspell \"**/*.{js,md,scss,json}\" --no-must-find-files",
"start": "gulp"
},
"devDependencies": {
"@babel/core": "^7.20.2",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-optional-chaining": "^7.18.9",
"@babel/preset-env": "^7.20.2",
"@sampotts/eslint-config": "1.1.7",
"autoprefixer": "^10.4.13",
"aws-sdk": "^2.1256.0",
"babel-eslint": "^10.1.0",
"browser-sync": "^2.27.10",
"colorette": "2.0.19",
"cspell": "^6.14.2",
"cssnano": "^5.1.14",
"del": "^6.1.1",
"eslint": "^7.23.0",
"fancy-log": "^2.0.0",
"git-branch": "^2.0.1",
"gulp": "^4.0.2",
"gulp-awspublish": "^6.0.2",
"gulp-better-rollup": "^4.0.1",
"gulp-filter": "^7.0.0",
"gulp-header": "^2.0.9",
"gulp-hub": "^4.2.0",
"gulp-if": "^3.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-open": "^3.0.1",
"gulp-plumber": "^1.2.1",
"gulp-postcss": "^9.0.1",
"gulp-rename": "^2.0.0",
"gulp-replace": "^1.1.3",
"gulp-sass": "^5.1.0",
"gulp-size": "^4.0.1",
"gulp-sourcemaps": "^3.0.0",
"gulp-svgstore": "^9.0.0",
"gulp-terser": "^2.1.0",
"postcss": "^8.4.19",
"postcss-custom-properties": "^12.1.9",
"postcss-scss": "^4.0.5",
"prettier-eslint": "^12.0.0",
"prettier-stylelint": "^0.4.2",
"remark-cli": "^11.0.0",
"remark-validate-links": "^12.1.0",
"rollup": "^3.3.0",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"sass": "^1.56.1",
"stylelint": "^14.15.0",
"stylelint-config-prettier": "^9.0.4",
"stylelint-config-sass-guidelines": "^9.0.1",
"stylelint-selector-bem-pattern": "^2.1.1"
},
"dependencies": {
"core-js": "^3.26.1",
"custom-event-polyfill": "^1.0.7",
"loadjs": "^4.2.0",
"rangetouch": "^2.0.1",
"url-polyfill": "^1.1.12"
}
}
+36 -26
View File
@@ -1,29 +1,39 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
// Linting
"stylelint.enable": true,
"css.validate": false,
"scss.validate": false,
"javascript.validate.enable": false,
// Prettier
"prettier.eslintIntegration": true,
"prettier.stylelintIntegration": true,
// Formatting
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
// Trim on save
"files.trimTrailingWhitespace": true
"folders": [
{
"path": "."
}
],
"settings": {
"search.exclude": {
"**/node_modules": true,
"**/dist": true
},
// Linting
"stylelint.enable": true,
"stylelint.validate": ["css", "scss"],
"css.validate": false,
"less.validate": false,
"scss.validate": false,
"javascript.validate.enable": false,
// Formatting
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
// Trim on save
"files.trimTrailingWhitespace": true,
// Special file associations
"files.associations": {
".eslintrc": "jsonc"
},
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}
}
+13882
View File
File diff suppressed because it is too large Load Diff
+333 -308
View File
@@ -8,12 +8,12 @@ import support from './support';
import { dedupe } from './utils/arrays';
import browser from './utils/browser';
import {
createElement,
emptyElement,
getAttributesFromSelector,
insertAfter,
removeElement,
toggleClass,
createElement,
emptyElement,
getAttributesFromSelector,
insertAfter,
removeElement,
toggleClass,
} from './utils/elements';
import { on, triggerEvent } from './utils/events';
import fetch from './utils/fetch';
@@ -23,363 +23,388 @@ import { getHTML } from './utils/strings';
import { parseUrl } from './utils/urls';
const captions = {
// Setup captions
setup() {
// Requires UI support
if (!this.supported.ui) {
return;
}
// Setup captions
setup() {
// Requires UI support
if (!this.supported.ui) {
return;
}
// Only Vimeo and HTML5 video supported at this point
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
// Clear menu and hide
if (
is.array(this.config.controls) &&
this.config.controls.includes('settings') &&
this.config.settings.includes('captions')
) {
controls.setCaptionsMenu.call(this);
}
// Only Vimeo and HTML5 video supported at this point
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
// Clear menu and hide
if (
is.array(this.config.controls) &&
this.config.controls.includes('settings') &&
this.config.settings.includes('captions')
) {
controls.setCaptionsMenu.call(this);
}
return;
}
return;
}
// Inject the container
if (!is.element(this.elements.captions)) {
this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
// Inject the container
if (!is.element(this.elements.captions)) {
this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
this.elements.captions.setAttribute('dir', 'auto');
insertAfter(this.elements.captions, this.elements.wrapper);
}
insertAfter(this.elements.captions, this.elements.wrapper);
}
// Fix IE captions if CORS is used
// Fetch captions and inject as blobs instead (data URIs not supported!)
if (browser.isIE && window.URL) {
const elements = this.media.querySelectorAll('track');
// Fix IE captions if CORS is used
// Fetch captions and inject as blobs instead (data URIs not supported!)
if (browser.isIE && window.URL) {
const elements = this.media.querySelectorAll('track');
Array.from(elements).forEach(track => {
const src = track.getAttribute('src');
const url = parseUrl(src);
Array.from(elements).forEach((track) => {
const src = track.getAttribute('src');
const url = parseUrl(src);
if (
url !== null &&
url.hostname !== window.location.href.hostname &&
['http:', 'https:'].includes(url.protocol)
) {
fetch(src, 'blob')
.then(blob => {
track.setAttribute('src', window.URL.createObjectURL(blob));
})
.catch(() => {
removeElement(track);
});
}
if (
url !== null &&
url.hostname !== window.location.href.hostname &&
['http:', 'https:'].includes(url.protocol)
) {
fetch(src, 'blob')
.then((blob) => {
track.setAttribute('src', window.URL.createObjectURL(blob));
})
.catch(() => {
removeElement(track);
});
}
});
}
// Get and set initial data
// The "preferred" options are not realized unless / until the wanted language has a match
// * languages: Array of user's browser languages.
// * language: The language preferred by user settings or config
// * active: The state preferred by user settings or config
// * toggled: The real captions state
// Get and set initial data
// The "preferred" options are not realized unless / until the wanted language has a match
// * languages: Array of user's browser languages.
// * language: The language preferred by user settings or config
// * active: The state preferred by user settings or config
// * toggled: The real captions state
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
const languages = dedupe(browserLanguages.map((language) => language.split('-')[0]));
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
// Use first browser language when language is 'auto'
if (language === 'auto') {
[language] = languages;
}
// Use first browser language when language is 'auto'
if (language === 'auto') {
[language] = languages;
}
let active = this.storage.get('captions');
if (!is.boolean(active)) {
({ active } = this.config.captions);
}
let active = this.storage.get('captions');
if (!is.boolean(active)) {
({ active } = this.config.captions);
}
Object.assign(this.captions, {
toggled: false,
active,
language,
languages,
});
Object.assign(this.captions, {
toggled: false,
active,
language,
languages,
// Watch changes to textTracks and update captions menu
if (this.isHTML5) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
}
// Update available languages in list next tick (the event must not be triggered before the listeners)
setTimeout(captions.update.bind(this), 0);
},
// Update available language options in settings based on tracks
update() {
const tracks = captions.getTracks.call(this, true);
// Get the wanted language
const { active, language, meta, currentTrackNode } = this.captions;
const languageExists = Boolean(tracks.find((track) => track.language === language));
// Handle tracks (add event listener and "pseudo"-default)
if (this.isHTML5 && this.isVideo) {
tracks
.filter((track) => !meta.get(track))
.forEach((track) => {
this.debug.log('Track added', track);
// Attempt to store if the original dom element was "default"
meta.set(track, {
default: track.mode === 'showing',
});
// Turn off native caption rendering to avoid double captions
// Note: mode='hidden' forces a track to download. To ensure every track
// isn't downloaded at once, only 'showing' tracks should be reassigned
// eslint-disable-next-line no-param-reassign
if (track.mode === 'showing') {
// eslint-disable-next-line no-param-reassign
track.mode = 'hidden';
}
// Add event listener for cue changes
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
});
}
// Watch changes to textTracks and update captions menu
if (this.isHTML5) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
}
// Update language first time it matches, or if the previous matching track was removed
if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
captions.setLanguage.call(this, language);
captions.toggle.call(this, active && languageExists);
}
// Update available languages in list next tick (the event must not be triggered before the listeners)
setTimeout(captions.update.bind(this), 0);
},
// Enable or disable captions based on track length
if (this.elements) {
toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
}
// Update available language options in settings based on tracks
update() {
const tracks = captions.getTracks.call(this, true);
// Get the wanted language
const { active, language, meta, currentTrackNode } = this.captions;
const languageExists = Boolean(tracks.find(track => track.language === language));
// Update available languages in list
if (
is.array(this.config.controls) &&
this.config.controls.includes('settings') &&
this.config.settings.includes('captions')
) {
controls.setCaptionsMenu.call(this);
}
},
// Handle tracks (add event listener and "pseudo"-default)
if (this.isHTML5 && this.isVideo) {
tracks.filter(track => !meta.get(track)).forEach(track => {
this.debug.log('Track added', track);
// Attempt to store if the original dom element was "default"
meta.set(track, {
default: track.mode === 'showing',
});
// Toggle captions display
// Used internally for the toggleCaptions method, with the passive option forced to false
toggle(input, passive = true) {
// If there's no full support
if (!this.supported.ui) {
return;
}
// Turn off native caption rendering to avoid double captions
track.mode = 'hidden';
const { toggled } = this.captions; // Current state
const activeClass = this.config.classNames.captions.active;
// Get the next state
// If the method is called without parameter, toggle based on current value
const active = is.nullOrUndefined(input) ? !toggled : input;
// Add event listener for cue changes
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
});
}
// Update state and trigger event
if (active !== toggled) {
// When passive, don't override user preferences
if (!passive) {
this.captions.active = active;
this.storage.set({ captions: active });
}
// Update language first time it matches, or if the previous matching track was removed
if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
captions.setLanguage.call(this, language);
captions.toggle.call(this, active && languageExists);
}
// Enable or disable captions based on track length
toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
// Update available languages in list
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
controls.setCaptionsMenu.call(this);
}
},
// Toggle captions display
// Used internally for the toggleCaptions method, with the passive option forced to false
toggle(input, passive = true) {
// If there's no full support
if (!this.supported.ui) {
return;
}
const { toggled } = this.captions; // Current state
const activeClass = this.config.classNames.captions.active;
// Get the next state
// If the method is called without parameter, toggle based on current value
const active = is.nullOrUndefined(input) ? !toggled : input;
// Update state and trigger event
if (active !== toggled) {
// When passive, don't override user preferences
if (!passive) {
this.captions.active = active;
this.storage.set({ captions: active });
}
// Force language if the call isn't passive and there is no matching language to toggle to
if (!this.language && active && !passive) {
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);
// Override user preferences to avoid switching languages if a matching track is added
this.captions.language = track.language;
// Set caption, but don't store in localStorage as user preference
captions.set.call(this, tracks.indexOf(track));
return;
}
// Toggle button if it's enabled
if (this.elements.buttons.captions) {
this.elements.buttons.captions.pressed = active;
}
// Add class hook
toggleClass(this.elements.container, activeClass, active);
this.captions.toggled = active;
// Update settings menu
controls.updateSetting.call(this, 'captions');
// Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
}
},
// Set captions by track index
// Used internally for the currentTrack setter with the passive option forced to false
set(index, passive = true) {
// Force language if the call isn't passive and there is no matching language to toggle to
if (!this.language && active && !passive) {
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);
// Disable captions if setting to -1
if (index === -1) {
captions.toggle.call(this, false, passive);
return;
}
// Override user preferences to avoid switching languages if a matching track is added
this.captions.language = track.language;
if (!is.number(index)) {
this.debug.warn('Invalid caption argument', index);
return;
}
// Set caption, but don't store in localStorage as user preference
captions.set.call(this, tracks.indexOf(track));
return;
}
if (!(index in tracks)) {
this.debug.warn('Track not found', index);
return;
}
// Toggle button if it's enabled
if (this.elements.buttons.captions) {
this.elements.buttons.captions.pressed = active;
}
if (this.captions.currentTrack !== index) {
this.captions.currentTrack = index;
const track = tracks[index];
const { language } = track || {};
// Add class hook
toggleClass(this.elements.container, activeClass, active);
// Store reference to node for invalidation on remove
this.captions.currentTrackNode = track;
this.captions.toggled = active;
// Update settings menu
controls.updateSetting.call(this, 'captions');
// Update settings menu
controls.updateSetting.call(this, 'captions');
// When passive, don't override user preferences
if (!passive) {
this.captions.language = language;
this.storage.set({ language });
}
// Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
}
// Handle Vimeo captions
if (this.isVimeo) {
this.embed.enableTextTrack(language);
}
// Wait for the call stack to clear before setting mode='hidden'
// on the active track - forcing the browser to download it
setTimeout(() => {
if (active && this.captions.toggled) {
this.captions.currentTrackNode.mode = 'hidden';
}
});
},
// Trigger event
triggerEvent.call(this, this.media, 'languagechange');
}
// Set captions by track index
// Used internally for the currentTrack setter with the passive option forced to false
set(index, passive = true) {
const tracks = captions.getTracks.call(this);
// Show captions
captions.toggle.call(this, true, passive);
// Disable captions if setting to -1
if (index === -1) {
captions.toggle.call(this, false, passive);
return;
}
if (this.isHTML5 && this.isVideo) {
// If we change the active track while a cue is already displayed we need to update it
captions.updateCues.call(this);
}
},
if (!is.number(index)) {
this.debug.warn('Invalid caption argument', index);
return;
}
// Set captions by language
// Used internally for the language setter with the passive option forced to false
setLanguage(input, passive = true) {
if (!is.string(input)) {
this.debug.warn('Invalid language argument', input);
return;
}
// Normalize
const language = input.toLowerCase();
if (!(index in tracks)) {
this.debug.warn('Track not found', index);
return;
}
if (this.captions.currentTrack !== index) {
this.captions.currentTrack = index;
const track = tracks[index];
const { language } = track || {};
// Store reference to node for invalidation on remove
this.captions.currentTrackNode = track;
// Update settings menu
controls.updateSetting.call(this, 'captions');
// When passive, don't override user preferences
if (!passive) {
this.captions.language = language;
this.storage.set({ language });
}
// Set currentTrack
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [language]);
captions.set.call(this, tracks.indexOf(track), passive);
},
// Handle Vimeo captions
if (this.isVimeo) {
this.embed.enableTextTrack(language);
}
// Get current valid caption tracks
// If update is false it will also ignore tracks without metadata
// This is used to "freeze" the language options when captions.update is false
getTracks(update = false) {
// Handle media or textTracks missing or null
const tracks = Array.from((this.media || {}).textTracks || []);
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
.filter(track => ['captions', 'subtitles'].includes(track.kind));
},
// Trigger event
triggerEvent.call(this, this.media, 'languagechange');
}
// Match tracks based on languages and get the first
findTrack(languages, force = false) {
const tracks = captions.getTracks.call(this);
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
let track;
languages.every(language => {
track = sorted.find(track => track.language === language);
return !track; // Break iteration if there is a match
});
// If no match is found but is required, get first
return track || (force ? sorted[0] : undefined);
},
// Show captions
captions.toggle.call(this, true, passive);
// Get the current track
getCurrentTrack() {
return captions.getTracks.call(this)[this.currentTrack];
},
if (this.isHTML5 && this.isVideo) {
// If we change the active track while a cue is already displayed we need to update it
captions.updateCues.call(this);
}
},
// Get UI label for track
getLabel(track) {
let currentTrack = track;
// Set captions by language
// Used internally for the language setter with the passive option forced to false
setLanguage(input, passive = true) {
if (!is.string(input)) {
this.debug.warn('Invalid language argument', input);
return;
}
// Normalize
const language = input.toLowerCase();
this.captions.language = language;
if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
currentTrack = captions.getCurrentTrack.call(this);
}
// Set currentTrack
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [language]);
captions.set.call(this, tracks.indexOf(track), passive);
},
if (is.track(currentTrack)) {
if (!is.empty(currentTrack.label)) {
return currentTrack.label;
}
// Get current valid caption tracks
// If update is false it will also ignore tracks without metadata
// This is used to "freeze" the language options when captions.update is false
getTracks(update = false) {
// Handle media or textTracks missing or null
const tracks = Array.from((this.media || {}).textTracks || []);
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks
.filter((track) => !this.isHTML5 || update || this.captions.meta.has(track))
.filter((track) => ['captions', 'subtitles'].includes(track.kind));
},
if (!is.empty(currentTrack.language)) {
return track.language.toUpperCase();
}
// Match tracks based on languages and get the first
findTrack(languages, force = false) {
const tracks = captions.getTracks.call(this);
const sortIsDefault = (track) => Number((this.captions.meta.get(track) || {}).default);
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
let track;
return i18n.get('enabled', this.config);
}
languages.every((language) => {
track = sorted.find((t) => t.language === language);
return !track; // Break iteration if there is a match
});
return i18n.get('disabled', this.config);
},
// If no match is found but is required, get first
return track || (force ? sorted[0] : undefined);
},
// Update captions using current track's active cues
// Also optional array argument in case there isn't any track (ex: vimeo)
updateCues(input) {
// Requires UI
if (!this.supported.ui) {
return;
}
// Get the current track
getCurrentTrack() {
return captions.getTracks.call(this)[this.currentTrack];
},
if (!is.element(this.elements.captions)) {
this.debug.warn('No captions element to render to');
return;
}
// Get UI label for track
getLabel(track) {
let currentTrack = track;
// Only accept array or empty input
if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
this.debug.warn('updateCues: Invalid input', input);
return;
}
if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
currentTrack = captions.getCurrentTrack.call(this);
}
let cues = input;
if (is.track(currentTrack)) {
if (!is.empty(currentTrack.label)) {
return currentTrack.label;
}
// Get cues from track
if (!cues) {
const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || [])
.map(cue => cue.getCueAsHTML())
.map(getHTML);
}
if (!is.empty(currentTrack.language)) {
return track.language.toUpperCase();
}
// Set new caption text
const content = cues.map(cueText => cueText.trim()).join('\n');
const changed = content !== this.elements.captions.innerHTML;
return i18n.get('enabled', this.config);
}
if (changed) {
// Empty the container and create a new child element
emptyElement(this.elements.captions);
const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
caption.innerHTML = content;
this.elements.captions.appendChild(caption);
return i18n.get('disabled', this.config);
},
// Trigger event
triggerEvent.call(this, this.media, 'cuechange');
}
},
// Update captions using current track's active cues
// Also optional array argument in case there isn't any track (ex: vimeo)
updateCues(input) {
// Requires UI
if (!this.supported.ui) {
return;
}
if (!is.element(this.elements.captions)) {
this.debug.warn('No captions element to render to');
return;
}
// Only accept array or empty input
if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
this.debug.warn('updateCues: Invalid input', input);
return;
}
let cues = input;
// Get cues from track
if (!cues) {
const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || [])
.map((cue) => cue.getCueAsHTML())
.map(getHTML);
}
// Set new caption text
const content = cues.map((cueText) => cueText.trim()).join('\n');
const changed = content !== this.elements.captions.innerHTML;
if (changed) {
// Empty the container and create a new child element
emptyElement(this.elements.captions);
const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
caption.innerHTML = content;
this.elements.captions.appendChild(caption);
// Trigger event
triggerEvent.call(this, this.media, 'cuechange');
}
},
};
export default captions;
+441 -418
View File
@@ -3,433 +3,456 @@
// ==========================================================================
const defaults = {
// Disable
// Disable
enabled: true,
// Custom media title
title: '',
// Logging to console
debug: false,
// Auto play (if supported)
autoplay: false,
// Only allow one media playing at once (vimeo only)
autopause: true,
// Allow inline playback on iOS
playsinline: true,
// Default time to skip when rewind/fast forward
seekTime: 10,
// Default volume
volume: 1,
muted: false,
// Pass a custom duration
duration: null,
// Display the media duration on load in the current time position
// If you have opted to display both duration and currentTime, this is ignored
displayDuration: true,
// Invert the current time to be a countdown
invertTime: true,
// Clicking the currentTime inverts it's value to show time left rather than elapsed
toggleInvert: true,
// Force an aspect ratio
// The format must be `'w:h'` (e.g. `'16:9'`)
ratio: null,
// Click video container to play/pause
clickToPlay: true,
// Auto hide the controls
hideControls: true,
// Reset to start when playback ended
resetOnEnd: false,
// Disable the standard context menu
disableContextMenu: true,
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.7.7/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
// Quality default
quality: {
default: 576,
// The options to display in the UI, if available for the source media
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
forced: false,
onChange: null,
},
// Set loops
loop: {
active: false,
// start: null,
// end: null,
},
// Speed default and options to display
speed: {
selected: 1,
// The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],
},
// Keyboard shortcut settings
keyboard: {
focused: true,
global: false,
},
// Display tooltips
tooltips: {
controls: false,
seek: true,
},
// Captions settings
captions: {
active: false,
language: 'auto',
// Listen to new tracks added after Plyr is initialized.
// This is needed for streaming captions, but may result in unselectable options
update: false,
},
// Fullscreen settings
fullscreen: {
enabled: true, // Allow fullscreen?
fallback: true, // Fallback using full viewport/window
iosNative: false, // Use the native fullscreen in iOS (disables custom controls)
// Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode
// Non-ancestors of the player element will be ignored
// container: null, // defaults to the player element
},
// Local storage
storage: {
enabled: true,
key: 'plyr',
},
// Custom media title
title: '',
// Default controls
controls: [
'play-large',
// 'restart',
// 'rewind',
'play',
// 'fast-forward',
'progress',
'current-time',
// 'duration',
'mute',
'volume',
'captions',
'settings',
'pip',
'airplay',
// 'download',
'fullscreen',
],
settings: ['captions', 'quality', 'speed'],
// Logging to console
debug: false,
// Auto play (if supported)
autoplay: false,
// Only allow one media playing at once (vimeo only)
autopause: true,
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
playsinline: true,
// Default time to skip when rewind/fast forward
seekTime: 10,
// Default volume
volume: 1,
muted: false,
// Pass a custom duration
duration: null,
// Display the media duration on load in the current time position
// If you have opted to display both duration and currentTime, this is ignored
displayDuration: true,
// Invert the current time to be a countdown
invertTime: true,
// Clicking the currentTime inverts it's value to show time left rather than elapsed
toggleInvert: true,
// Aspect ratio (for embeds)
ratio: '16:9',
// Click video container to play/pause
clickToPlay: true,
// Auto hide the controls
hideControls: true,
// Reset to start when playback ended
resetOnEnd: false,
// Disable the standard context menu
disableContextMenu: true,
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.5.2/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
// Quality default
quality: {
default: 576,
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
// Localisation
i18n: {
restart: 'Restart',
rewind: 'Rewind {seektime}s',
play: 'Play',
pause: 'Pause',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
duration: 'Duration',
volume: 'Volume',
mute: 'Mute',
unmute: 'Unmute',
enableCaptions: 'Enable captions',
disableCaptions: 'Disable captions',
download: 'Download',
enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen',
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
pip: 'PIP',
menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
loop: 'Loop',
start: 'Start',
end: 'End',
all: 'All',
reset: 'Reset',
disabled: 'Disabled',
enabled: 'Enabled',
advertisement: 'Ad',
qualityBadge: {
2160: '4K',
1440: 'HD',
1080: 'HD',
720: 'HD',
576: 'SD',
480: 'SD',
},
},
// Set loops
loop: {
active: false,
// start: null,
// end: null,
},
// Speed default and options to display
speed: {
selected: 1,
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
},
// Keyboard shortcut settings
keyboard: {
focused: true,
global: false,
},
// Display tooltips
tooltips: {
controls: false,
seek: true,
},
// Captions settings
captions: {
active: false,
language: 'auto',
// Listen to new tracks added after Plyr is initialized.
// This is needed for streaming captions, but may result in unselectable options
update: false,
},
// Fullscreen settings
fullscreen: {
enabled: true, // Allow fullscreen?
fallback: true, // Fallback using full viewport/window
iosNative: false, // Use the native fullscreen in iOS (disables custom controls)
},
// Local storage
storage: {
enabled: true,
key: 'plyr',
},
// Default controls
controls: [
'play-large',
// 'restart',
// 'rewind',
'play',
// 'fast-forward',
'progress',
'current-time',
'mute',
'volume',
'captions',
'settings',
'pip',
'airplay',
// 'download',
'fullscreen',
],
settings: ['captions', 'quality', 'speed'],
// Localisation
i18n: {
restart: 'Restart',
rewind: 'Rewind {seektime}s',
play: 'Play',
pause: 'Pause',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
duration: 'Duration',
volume: 'Volume',
mute: 'Mute',
unmute: 'Unmute',
enableCaptions: 'Enable captions',
disableCaptions: 'Disable captions',
download: 'Download',
enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen',
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
loop: 'Loop',
start: 'Start',
end: 'End',
all: 'All',
reset: 'Reset',
disabled: 'Disabled',
enabled: 'Enabled',
advertisement: 'Ad',
qualityBadge: {
2160: '4K',
1440: 'HD',
1080: 'HD',
720: 'HD',
576: 'SD',
480: 'SD',
},
},
// URLs
urls: {
download: null,
vimeo: {
sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}',
api: 'https://vimeo.com/api/v2/video/{0}.json',
},
youtube: {
sdk: 'https://www.youtube.com/iframe_api',
api:
'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
},
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
},
},
// Custom control listeners
listeners: {
seek: null,
play: null,
pause: null,
restart: null,
rewind: null,
fastForward: null,
mute: null,
volume: null,
captions: null,
download: null,
fullscreen: null,
pip: null,
airplay: null,
speed: null,
quality: null,
loop: null,
language: null,
},
// Events to watch and bubble
events: [
// Events to watch on HTML5 media elements and bubble
// https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
'ended',
'progress',
'stalled',
'playing',
'waiting',
'canplay',
'canplaythrough',
'loadstart',
'loadeddata',
'loadedmetadata',
'timeupdate',
'volumechange',
'play',
'pause',
'error',
'seeking',
'seeked',
'emptied',
'ratechange',
'cuechange',
// Custom events
'download',
'enterfullscreen',
'exitfullscreen',
'captionsenabled',
'captionsdisabled',
'languagechange',
'controlshidden',
'controlsshown',
'ready',
// YouTube
'statechange',
// Quality
'qualitychange',
// Ads
'adsloaded',
'adscontentpause',
'adscontentresume',
'adstarted',
'adsmidpoint',
'adscomplete',
'adsallcomplete',
'adsimpression',
'adsclick',
],
// Selectors
// Change these to match your template if using custom HTML
selectors: {
editable: 'input, textarea, select, [contenteditable]',
container: '.plyr',
controls: {
container: null,
wrapper: '.plyr__controls',
},
labels: '[data-plyr]',
buttons: {
play: '[data-plyr="play"]',
pause: '[data-plyr="pause"]',
restart: '[data-plyr="restart"]',
rewind: '[data-plyr="rewind"]',
fastForward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]',
download: '[data-plyr="download"]',
fullscreen: '[data-plyr="fullscreen"]',
pip: '[data-plyr="pip"]',
airplay: '[data-plyr="airplay"]',
settings: '[data-plyr="settings"]',
loop: '[data-plyr="loop"]',
},
inputs: {
seek: '[data-plyr="seek"]',
volume: '[data-plyr="volume"]',
speed: '[data-plyr="speed"]',
language: '[data-plyr="language"]',
quality: '[data-plyr="quality"]',
},
display: {
currentTime: '.plyr__time--current',
duration: '.plyr__time--duration',
buffer: '.plyr__progress__buffer',
loop: '.plyr__progress__loop', // Used later
volume: '.plyr__volume--display',
},
progress: '.plyr__progress',
captions: '.plyr__captions',
caption: '.plyr__caption',
menu: {
quality: '.js-plyr__menu__list--quality',
},
},
// Class hooks added to the player in different states
classNames: {
type: 'plyr--{0}',
provider: 'plyr--{0}',
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster',
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading',
hover: 'plyr--hover',
tooltip: 'plyr__tooltip',
cues: 'plyr__cues',
hidden: 'plyr__sr-only',
hideControls: 'plyr--hide-controls',
isIos: 'plyr--is-ios',
isTouch: 'plyr--is-touch',
uiSupported: 'plyr--full-ui',
noTransition: 'plyr--no-transition',
display: {
time: 'plyr__time',
},
menu: {
value: 'plyr__menu__value',
badge: 'plyr__badge',
open: 'plyr--menu-open',
},
captions: {
enabled: 'plyr--captions-enabled',
active: 'plyr--captions-active',
},
fullscreen: {
enabled: 'plyr--fullscreen-enabled',
fallback: 'plyr--fullscreen-fallback',
},
pip: {
supported: 'plyr--pip-supported',
active: 'plyr--pip-active',
},
airplay: {
supported: 'plyr--airplay-supported',
active: 'plyr--airplay-active',
},
tabFocus: 'plyr__tab-focus',
previewThumbnails: {
// Tooltip thumbs
thumbContainer: 'plyr__preview-thumb',
thumbContainerShown: 'plyr__preview-thumb--is-shown',
imageContainer: 'plyr__preview-thumb__image-container',
timeContainer: 'plyr__preview-thumb__time-container',
// Scrubbing
scrubbingContainer: 'plyr__preview-scrubbing',
scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',
},
},
// Embed attributes
attributes: {
embed: {
provider: 'data-plyr-provider',
id: 'data-plyr-embed-id',
},
},
// API keys
keys: {
google: null,
},
// Advertisements plugin
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: {
enabled: false,
publisherId: '',
tagUrl: '',
},
// Preview Thumbnails plugin
previewThumbnails: {
enabled: false,
src: '',
},
// Vimeo plugin
// URLs
urls: {
download: null,
vimeo: {
byline: false,
portrait: false,
title: false,
speed: true,
transparent: false,
sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}',
api: 'https://vimeo.com/api/oembed.json?url={0}',
},
// YouTube plugin
youtube: {
noCookie: false, // Whether to use an alternative version of YouTube without cookies
rel: 0, // No related vids
showinfo: 0, // Hide info
iv_load_policy: 3, // Hide annotations
modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)
sdk: 'https://www.youtube.com/iframe_api',
api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',
},
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
},
},
// Custom control listeners
listeners: {
seek: null,
play: null,
pause: null,
restart: null,
rewind: null,
fastForward: null,
mute: null,
volume: null,
captions: null,
download: null,
fullscreen: null,
pip: null,
airplay: null,
speed: null,
quality: null,
loop: null,
language: null,
},
// Events to watch and bubble
events: [
// Events to watch on HTML5 media elements and bubble
// https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
'ended',
'progress',
'stalled',
'playing',
'waiting',
'canplay',
'canplaythrough',
'loadstart',
'loadeddata',
'loadedmetadata',
'timeupdate',
'volumechange',
'play',
'pause',
'error',
'seeking',
'seeked',
'emptied',
'ratechange',
'cuechange',
// Custom events
'download',
'enterfullscreen',
'exitfullscreen',
'captionsenabled',
'captionsdisabled',
'languagechange',
'controlshidden',
'controlsshown',
'ready',
// YouTube
'statechange',
// Quality
'qualitychange',
// Ads
'adsloaded',
'adscontentpause',
'adscontentresume',
'adstarted',
'adsmidpoint',
'adscomplete',
'adsallcomplete',
'adsimpression',
'adsclick',
],
// Selectors
// Change these to match your template if using custom HTML
selectors: {
editable: 'input, textarea, select, [contenteditable]',
container: '.plyr',
controls: {
container: null,
wrapper: '.plyr__controls',
},
labels: '[data-plyr]',
buttons: {
play: '[data-plyr="play"]',
pause: '[data-plyr="pause"]',
restart: '[data-plyr="restart"]',
rewind: '[data-plyr="rewind"]',
fastForward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]',
download: '[data-plyr="download"]',
fullscreen: '[data-plyr="fullscreen"]',
pip: '[data-plyr="pip"]',
airplay: '[data-plyr="airplay"]',
settings: '[data-plyr="settings"]',
loop: '[data-plyr="loop"]',
},
inputs: {
seek: '[data-plyr="seek"]',
volume: '[data-plyr="volume"]',
speed: '[data-plyr="speed"]',
language: '[data-plyr="language"]',
quality: '[data-plyr="quality"]',
},
display: {
currentTime: '.plyr__time--current',
duration: '.plyr__time--duration',
buffer: '.plyr__progress__buffer',
loop: '.plyr__progress__loop', // Used later
volume: '.plyr__volume--display',
},
progress: '.plyr__progress',
captions: '.plyr__captions',
caption: '.plyr__caption',
},
// Class hooks added to the player in different states
classNames: {
type: 'plyr--{0}',
provider: 'plyr--{0}',
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',
embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster',
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading',
hover: 'plyr--hover',
tooltip: 'plyr__tooltip',
cues: 'plyr__cues',
marker: 'plyr__progress__marker',
hidden: 'plyr__sr-only',
hideControls: 'plyr--hide-controls',
isTouch: 'plyr--is-touch',
uiSupported: 'plyr--full-ui',
noTransition: 'plyr--no-transition',
display: {
time: 'plyr__time',
},
menu: {
value: 'plyr__menu__value',
badge: 'plyr__badge',
open: 'plyr--menu-open',
},
captions: {
enabled: 'plyr--captions-enabled',
active: 'plyr--captions-active',
},
fullscreen: {
enabled: 'plyr--fullscreen-enabled',
fallback: 'plyr--fullscreen-fallback',
},
pip: {
supported: 'plyr--pip-supported',
active: 'plyr--pip-active',
},
airplay: {
supported: 'plyr--airplay-supported',
active: 'plyr--airplay-active',
},
previewThumbnails: {
// Tooltip thumbs
thumbContainer: 'plyr__preview-thumb',
thumbContainerShown: 'plyr__preview-thumb--is-shown',
imageContainer: 'plyr__preview-thumb__image-container',
timeContainer: 'plyr__preview-thumb__time-container',
// Scrubbing
scrubbingContainer: 'plyr__preview-scrubbing',
scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',
},
},
// Embed attributes
attributes: {
embed: {
provider: 'data-plyr-provider',
id: 'data-plyr-embed-id',
hash: 'data-plyr-embed-hash',
},
},
// Advertisements plugin
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: {
enabled: false,
publisherId: '',
tagUrl: '',
},
// Preview Thumbnails plugin
previewThumbnails: {
enabled: false,
src: '',
},
// Vimeo plugin
vimeo: {
byline: false,
portrait: false,
title: false,
speed: true,
transparent: false,
// Custom settings from Plyr
customControls: true,
referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
// Whether the owner of the video has a Pro or Business account
// (which allows us to properly hide controls without CSS hacks, etc)
premium: false,
},
// YouTube plugin
youtube: {
rel: 0, // No related vids
showinfo: 0, // Hide info
iv_load_policy: 3, // Hide annotations
modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)
// Custom settings from Plyr
customControls: true,
noCookie: false, // Whether to use an alternative version of YouTube without cookies
},
// Media Metadata
mediaMetadata: {
title: '',
artist: '',
album: '',
artwork: [],
},
// Markers
markers: {
enabled: false,
points: [],
},
};
export default defaults;
+2 -2
View File
@@ -3,8 +3,8 @@
// ==========================================================================
export const pip = {
active: 'picture-in-picture',
inactive: 'inline',
active: 'picture-in-picture',
inactive: 'inline',
};
export default { pip };
+14 -14
View File
@@ -3,14 +3,14 @@
// ==========================================================================
export const providers = {
html5: 'html5',
youtube: 'youtube',
vimeo: 'vimeo',
html5: 'html5',
youtube: 'youtube',
vimeo: 'vimeo',
};
export const types = {
audio: 'audio',
video: 'video',
audio: 'audio',
video: 'video',
};
/**
@@ -18,17 +18,17 @@ export const types = {
* @param {String} url
*/
export function getProviderByUrl(url) {
// YouTube
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) {
return providers.youtube;
}
// YouTube
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) {
return providers.youtube;
}
// Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo;
}
// Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo;
}
return null;
return null;
}
export default { providers, types };
+17 -17
View File
@@ -5,26 +5,26 @@
const noop = () => {};
export default class Console {
constructor(enabled = false) {
this.enabled = window.console && enabled;
constructor(enabled = false) {
this.enabled = window.console && enabled;
if (this.enabled) {
this.log('Debugging enabled');
}
if (this.enabled) {
this.log('Debugging enabled');
}
}
get log() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
}
get log() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
}
get warn() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
}
get warn() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
}
get error() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
}
get error() {
// eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
}
}
+1711 -1580
View File
File diff suppressed because it is too large Load Diff
+239 -217
View File
@@ -4,41 +4,166 @@
// https://webkit.org/blog/7929/designing-websites-for-iphone-x/
// ==========================================================================
import { repaint } from './utils/animation';
import browser from './utils/browser';
import { hasClass, toggleClass, trapFocus } from './utils/elements';
import { closest, getElements, hasClass, toggleClass } from './utils/elements';
import { on, triggerEvent } from './utils/events';
import is from './utils/is';
import { silencePromise } from './utils/promise';
function onChange() {
if (!this.enabled) {
class Fullscreen {
constructor(player) {
// Keep reference to parent
this.player = player;
// Get prefix
this.prefix = Fullscreen.prefix;
this.property = Fullscreen.property;
// Scroll position
this.scrollPosition = { x: 0, y: 0 };
// Force the use of 'full window/browser' rather than fullscreen
this.forceFallback = player.config.fullscreen.fallback === 'force';
// Get the fullscreen element
// Checks container is an ancestor, defaults to null
this.player.elements.fullscreen =
player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container);
// Register event listeners
// Handle event (incase user presses escape etc)
on.call(
this.player,
document,
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => {
// TODO: Filter for target??
this.onChange();
},
);
// Fullscreen toggle on double click
on.call(this.player, this.player.elements.container, 'dblclick', (event) => {
// Ignore double click in controls
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return;
}
this.player.listeners.proxy(event, this.toggle, 'fullscreen');
});
// Tap focus when in fullscreen
on.call(this, this.player.elements.container, 'keydown', (event) => this.trapFocus(event));
// Update the UI
this.update();
}
// Determine if native supported
static get nativeSupported() {
return !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled
);
}
// If we're actually using native
get useNative() {
return Fullscreen.nativeSupported && !this.forceFallback;
}
// Get the prefix for handlers
static get prefix() {
// No prefix
if (is.function(document.exitFullscreen)) return '';
// Check for fullscreen support by vendor prefix
let value = '';
const prefixes = ['webkit', 'moz', 'ms'];
prefixes.some((pre) => {
if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
value = pre;
return true;
}
return false;
});
return value;
}
static get property() {
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
}
// Determine if fullscreen is supported
get supported() {
return [
// Fullscreen is enabled in config
this.player.config.fullscreen.enabled,
// Must be a video
this.player.isVideo,
// Either native is supported or fallback enabled
Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,
// YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline
// must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback
!this.player.isYouTube ||
Fullscreen.nativeSupported ||
!browser.isIos ||
(this.player.config.playsinline && !this.player.config.fullscreen.iosNative),
].every(Boolean);
}
// Get active state
get active() {
if (!this.supported) return false;
// Fallback using classname
if (!Fullscreen.nativeSupported || this.forceFallback) {
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
}
const element = !this.prefix
? this.target.getRootNode().fullscreenElement
: this.target.getRootNode()[`${this.prefix}${this.property}Element`];
return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;
}
// Get target element
get target() {
return browser.isIos && this.player.config.fullscreen.iosNative
? this.player.media
: this.player.elements.fullscreen ?? this.player.elements.container;
}
onChange = () => {
if (!this.supported) return;
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (is.element(button)) {
button.pressed = this.active;
button.pressed = this.active;
}
// Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up
const target = this.target === this.player.media ? this.target : this.player.elements.container;
// Trigger an event
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
};
// Trap focus in container
if (!browser.isIos) {
trapFocus.call(this.player, this.target, this.active);
}
}
function toggleFallback(toggle = false) {
toggleFallback = (toggle = false) => {
// Store or restore scroll position
if (toggle) {
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
this.scrollPosition = {
x: window.scrollX ?? 0,
y: window.scrollY ?? 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
}
// Toggle scroll
@@ -49,223 +174,120 @@ function toggleFallback(toggle = false) {
// Force full viewport on iPhone X+
if (browser.isIos) {
let viewport = document.head.querySelector('meta[name="viewport"]');
const property = 'viewport-fit=cover';
let viewport = document.head.querySelector('meta[name="viewport"]');
const property = 'viewport-fit=cover';
// Inject the viewport meta if required
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
}
// Inject the viewport meta if required
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
}
// Check if the property already exists
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
// Check if the property already exists
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) {
viewport.content += `,${property}`;
}
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
.filter(part => part.trim() !== property)
.join(',');
}
// Force a repaint as sometimes Safari doesn't want to fill the screen
setTimeout(() => repaint(this.target), 100);
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) viewport.content += `,${property}`;
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
.filter((part) => part.trim() !== property)
.join(',');
}
}
// Toggle button and fire events
onChange.call(this);
}
this.onChange();
};
class Fullscreen {
constructor(player) {
// Keep reference to parent
this.player = player;
// Trap focus inside container
trapFocus = (event) => {
// Bail if iOS/iPadOS, not active, not the tab key
if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;
// Get prefix
this.prefix = Fullscreen.prefix;
this.property = Fullscreen.property;
// Get the current focused element
const focused = document.activeElement;
const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');
const [first] = focusable;
const last = focusable[focusable.length - 1];
// Scroll position
this.scrollPosition = { x: 0, y: 0 };
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();
}
};
// Force the use of 'full window/browser' rather than fullscreen
this.forceFallback = player.config.fullscreen.fallback === 'force';
// Update UI
update = () => {
if (this.supported) {
let mode;
// 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);
},
);
if (this.forceFallback) mode = 'Fallback (forced)';
else if (Fullscreen.nativeSupported) mode = 'Native';
else mode = 'Fallback';
// 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();
this.player.debug.log(`${mode} fullscreen enabled`);
} else {
this.player.debug.log('Fullscreen not supported and fallback disabled');
}
// Determine if native supported
static get native() {
return !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled
);
// Add styling hook to show button
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);
};
// Make an element fullscreen
enter = () => {
if (!this.supported) return;
// iOS native fullscreen doesn't need the request step
if (browser.isIos && this.player.config.fullscreen.iosNative) {
if (this.player.isVimeo) {
this.player.embed.requestFullscreen();
} else {
this.target.webkitEnterFullscreen();
}
} else if (!Fullscreen.nativeSupported || this.forceFallback) {
this.toggleFallback(true);
} else if (!this.prefix) {
this.target.requestFullscreen({ navigationUI: 'hide' });
} else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`]();
}
};
// If we're actually using native
get usingNative() {
return Fullscreen.native && !this.forceFallback;
// Bail from fullscreen
exit = () => {
if (!this.supported) return;
// iOS native fullscreen
if (browser.isIos && this.player.config.fullscreen.iosNative) {
if (this.player.isVimeo) {
this.player.embed.exitFullscreen();
} else {
this.target.webkitEnterFullscreen();
}
silencePromise(this.player.play());
} else if (!Fullscreen.nativeSupported || this.forceFallback) {
this.toggleFallback(false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.property}`]();
}
};
// Get the prefix for handlers
static get prefix() {
// No prefix
if (is.function(document.exitFullscreen)) {
return '';
}
// Check for fullscreen support by vendor prefix
let value = '';
const prefixes = ['webkit', 'moz', 'ms'];
prefixes.some(pre => {
if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
value = pre;
return true;
}
return false;
});
return value;
}
static get property() {
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
}
// Determine if fullscreen is enabled
get enabled() {
return (
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
this.player.config.fullscreen.enabled &&
this.player.supported.ui &&
this.player.isVideo
);
}
// Get active state
get active() {
if (!this.enabled) {
return false;
}
// Fallback using classname
if (!Fullscreen.native || this.forceFallback) {
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
}
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
return element === 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) {
let mode;
if (this.forceFallback) {
mode = 'Fallback (forced)';
} else if (Fullscreen.native) {
mode = 'Native';
} else {
mode = 'Fallback';
}
this.player.debug.log(`${mode} fullscreen enabled`);
} else {
this.player.debug.log('Fullscreen not supported and fallback disabled');
}
// Add styling hook to show button
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
}
// Make an element fullscreen
enter() {
if (!this.enabled) {
return;
}
// iOS native fullscreen doesn't need the request step
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitEnterFullscreen();
} else if (!Fullscreen.native || this.forceFallback) {
toggleFallback.call(this, true);
} else if (!this.prefix) {
this.target.requestFullscreen();
} else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`]();
}
}
// Bail from fullscreen
exit() {
if (!this.enabled) {
return;
}
// iOS native fullscreen
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen();
this.player.play();
} else if (!Fullscreen.native || this.forceFallback) {
toggleFallback.call(this, false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.property}`]();
}
}
// Toggle state
toggle() {
if (!this.active) {
this.enter();
} else {
this.exit();
}
}
// Toggle state
toggle = () => {
if (!this.active) this.enter();
else this.exit();
};
}
export default Fullscreen;
+124 -100
View File
@@ -6,118 +6,142 @@ import support from './support';
import { removeElement } from './utils/elements';
import { triggerEvent } from './utils/events';
import is from './utils/is';
import { silencePromise } from './utils/promise';
import { setAspectRatio } from './utils/style';
const html5 = {
getSources() {
if (!this.isHTML5) {
return [];
getSources() {
if (!this.isHTML5) {
return [];
}
const sources = Array.from(this.media.querySelectorAll('source'));
// Filter out unsupported sources (if type is specified)
return sources.filter((source) => {
const type = source.getAttribute('type');
if (is.empty(type)) {
return true;
}
return support.mime.call(this, type);
});
},
// Get quality levels
getQualityOptions() {
// Whether we're forcing all options (e.g. for streaming)
if (this.config.quality.forced) {
return this.config.quality.options;
}
// Get sizes from <source> elements
return html5.getSources
.call(this)
.map((source) => Number(source.getAttribute('size')))
.filter(Boolean);
},
setup() {
if (!this.isHTML5) {
return;
}
const player = this;
// Set speed options from config
player.options.speed = player.config.speed.options;
// Set aspect ratio if fixed
if (!is.empty(this.config.ratio)) {
setAspectRatio.call(player);
}
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
// Get sources
const sources = html5.getSources.call(player);
const source = sources.find((s) => s.getAttribute('src') === player.source);
// Return size, if match is found
return source && Number(source.getAttribute('size'));
},
set(input) {
if (player.quality === input) {
return;
}
const sources = Array.from(this.media.querySelectorAll('source'));
// If we're using an external handler...
if (player.config.quality.forced && is.function(player.config.quality.onChange)) {
player.config.quality.onChange(input);
} else {
// Get sources
const sources = html5.getSources.call(player);
// Get first match for requested size
const source = sources.find((s) => Number(s.getAttribute('size')) === input);
// Filter out unsupported sources (if type is specified)
return sources.filter(source => {
const type = source.getAttribute('type');
if (is.empty(type)) {
return true;
}
return support.mime.call(this, type);
});
},
// Get quality levels
getQualityOptions() {
// Get sizes from <source> elements
return html5.getSources
.call(this)
.map(source => Number(source.getAttribute('size')))
.filter(Boolean);
},
extend() {
if (!this.isHTML5) {
// No matching source found
if (!source) {
return;
}
// Get current state
const { currentTime, paused, preload, readyState, playbackRate } = player.media;
// Set new source
player.media.src = source.getAttribute('src');
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
if (preload !== 'none' || readyState) {
// Restore time
player.once('loadedmetadata', () => {
player.speed = playbackRate;
player.currentTime = currentTime;
// Resume playing
if (!paused) {
silencePromise(player.play());
}
});
// Load new source
player.media.load();
}
}
const player = this;
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
// Get sources
const sources = html5.getSources.call(player);
const source = sources.find(source => source.getAttribute('src') === player.source);
// Return size, if match is found
return source && Number(source.getAttribute('size'));
},
set(input) {
// Get sources
const sources = html5.getSources.call(player);
// Get first match for requested size
const source = sources.find(source => Number(source.getAttribute('size')) === input);
// No matching source found
if (!source) {
return;
}
// Get current state
const { currentTime, paused, preload, readyState } = player.media;
// Set new source
player.media.src = source.getAttribute('src');
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
if (preload !== 'none' || readyState) {
// Restore time
player.once('loadedmetadata', () => {
player.currentTime = currentTime;
// Resume playing
if (!paused) {
player.play();
}
});
// Load new source
player.media.load();
}
// Trigger change event
triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: input,
});
},
// Trigger change event
triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: input,
});
},
},
});
},
// Cancel current network requests
// Cancel current network requests
// See https://github.com/sampotts/plyr/issues/174
cancelRequests() {
if (!this.isHTML5) {
return;
}
// Remove child sources
removeElement(html5.getSources.call(this));
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
this.media.setAttribute('src', this.config.blankVideo);
// Load the new empty source
// This will cancel existing requests
// See https://github.com/sampotts/plyr/issues/174
cancelRequests() {
if (!this.isHTML5) {
return;
}
this.media.load();
// Remove child sources
removeElement(html5.getSources.call(this));
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
this.media.setAttribute('src', this.config.blankVideo);
// Load the new empty source
// This will cancel existing requests
// See https://github.com/sampotts/plyr/issues/174
this.media.load();
// Debugging
this.debug.log('Cancelled network requests');
},
// Debugging
this.debug.log('Cancelled network requests');
},
};
export default html5;
+804 -814
View File
File diff suppressed because it is too large Load Diff
+38 -38
View File
@@ -8,52 +8,52 @@ import youtube from './plugins/youtube';
import { createElement, toggleClass, wrap } from './utils/elements';
const media = {
// Setup media
setup() {
// If there's no media, bail
if (!this.media) {
this.debug.warn('No media element found!');
return;
}
// Setup media
setup() {
// If there's no media, bail
if (!this.media) {
this.debug.warn('No media element found!');
return;
}
// Add type class
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
// Add type class
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
// Add provider class
toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
// Add provider class
toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
// Add video class for embeds
// This will require changes if audio embeds are added
if (this.isEmbed) {
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
}
// Add video class for embeds
// This will require changes if audio embeds are added
if (this.isEmbed) {
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
}
// Inject the player wrapper
if (this.isVideo) {
// Create the wrapper div
this.elements.wrapper = createElement('div', {
class: this.config.classNames.video,
});
// Inject the player wrapper
if (this.isVideo) {
// Create the wrapper div
this.elements.wrapper = createElement('div', {
class: this.config.classNames.video,
});
// Wrap the video in a container
wrap(this.media, this.elements.wrapper);
// Wrap the video in a container
wrap(this.media, this.elements.wrapper);
// Faux poster container
this.elements.poster = createElement('div', {
class: this.config.classNames.poster,
});
// Poster image container
this.elements.poster = createElement('div', {
class: this.config.classNames.poster,
});
this.elements.wrapper.appendChild(this.elements.poster);
}
this.elements.wrapper.appendChild(this.elements.poster);
}
if (this.isHTML5) {
html5.extend.call(this);
} else if (this.isYouTube) {
youtube.setup.call(this);
} else if (this.isVimeo) {
vimeo.setup.call(this);
}
},
if (this.isHTML5) {
html5.setup.call(this);
} else if (this.isYouTube) {
youtube.setup.call(this);
} else if (this.isVimeo) {
vimeo.setup.call(this);
}
},
};
export default media;
+588 -566
View File
File diff suppressed because it is too large Load Diff
+697
View File
@@ -0,0 +1,697 @@
import { createElement } from '../utils/elements';
import { once } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import { clamp } from '../utils/numbers';
import { formatTime } from '../utils/time';
// Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg"
const parseVtt = (vttDataString) => {
const processedList = [];
const frames = vttDataString.split(/\r\n\r\n|\n\n|\r\r/);
frames.forEach((frame) => {
const result = {};
const lines = frame.split(/\r\n|\n|\r/);
lines.forEach((line) => {
if (!is.number(result.startTime)) {
// The line with start and end times on it is the first line of interest
const matchTimes = line.match(
/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,
); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT
if (matchTimes) {
result.startTime =
Number(matchTimes[1] || 0) * 60 * 60 +
Number(matchTimes[2]) * 60 +
Number(matchTimes[3]) +
Number(`0.${matchTimes[4]}`);
result.endTime =
Number(matchTimes[6] || 0) * 60 * 60 +
Number(matchTimes[7]) * 60 +
Number(matchTimes[8]) +
Number(`0.${matchTimes[9]}`);
}
} else if (!is.empty(line.trim()) && is.empty(result.text)) {
// If we already have the startTime, then we're definitely up to the text line(s)
const lineSplit = line.trim().split('#xywh=');
[result.text] = lineSplit;
// If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image
if (lineSplit[1]) {
[result.x, result.y, result.w, result.h] = lineSplit[1].split(',');
}
}
});
if (result.text) {
processedList.push(result);
}
});
return processedList;
};
/**
* Preview thumbnails for seek hover and scrubbing
* Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar
* Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed
*
* Notes:
* - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole
* - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails
* - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered
*/
const fitRatio = (ratio, outer) => {
const targetRatio = outer.width / outer.height;
const result = {};
if (ratio > targetRatio) {
result.width = outer.width;
result.height = (1 / ratio) * outer.width;
} else {
result.height = outer.height;
result.width = ratio * outer.height;
}
return result;
};
class PreviewThumbnails {
/**
* PreviewThumbnails constructor.
* @param {Plyr} player
* @return {PreviewThumbnails}
*/
constructor(player) {
this.player = player;
this.thumbnails = [];
this.loaded = false;
this.lastMouseMoveTime = Date.now();
this.mouseDown = false;
this.loadedImages = [];
this.elements = {
thumb: {},
scrubbing: {},
};
this.load();
}
get enabled() {
return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;
}
load = () => {
// Toggle the regular seek tooltip
if (this.player.elements.display.seekTooltip) {
this.player.elements.display.seekTooltip.hidden = this.enabled;
}
if (!this.enabled) return;
this.getThumbnails().then(() => {
if (!this.enabled) {
return;
}
// Render DOM elements
this.render();
// Check to see if thumb container size was specified manually in CSS
this.determineContainerAutoSizing();
// Set up listeners
this.listeners();
this.loaded = true;
});
};
// Download VTT files and parse them
getThumbnails = () => {
return new Promise((resolve) => {
const { src } = this.player.config.previewThumbnails;
if (is.empty(src)) {
throw new Error('Missing previewThumbnails.src config attribute');
}
// Resolve promise
const sortAndResolve = () => {
// Sort smallest to biggest (e.g., [120p, 480p, 1080p])
this.thumbnails.sort((x, y) => x.height - y.height);
this.player.debug.log('Preview thumbnails', this.thumbnails);
resolve();
};
// Via callback()
if (is.function(src)) {
src((thumbnails) => {
this.thumbnails = thumbnails;
sortAndResolve();
});
}
// VTT urls
else {
// If string, convert into single-element list
const urls = is.string(src) ? [src] : src;
// Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
const promises = urls.map((u) => this.getThumbnail(u));
// Resolve
Promise.all(promises).then(sortAndResolve);
}
});
};
// Process individual VTT file
getThumbnail = (url) => {
return new Promise((resolve) => {
fetch(url).then((response) => {
const thumbnail = {
frames: parseVtt(response),
height: null,
urlPrefix: '',
};
// If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file
// If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank
// If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file
if (
!thumbnail.frames[0].text.startsWith('/') &&
!thumbnail.frames[0].text.startsWith('http://') &&
!thumbnail.frames[0].text.startsWith('https://')
) {
thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
}
// Download the first frame, so that we can determine/set the height of this thumbnailsDef
const tempImage = new Image();
tempImage.onload = () => {
thumbnail.height = tempImage.naturalHeight;
thumbnail.width = tempImage.naturalWidth;
this.thumbnails.push(thumbnail);
resolve();
};
tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;
});
});
};
startMove = (event) => {
if (!this.loaded) return;
if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;
// Wait until media has a duration
if (!this.player.media.duration) return;
if (event.type === 'touchmove') {
// Calculate seek hover position as approx video seconds
this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);
} else {
// Calculate seek hover position as approx video seconds
const clientRect = this.player.elements.progress.getBoundingClientRect();
const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left);
this.seekTime = this.player.media.duration * (percentage / 100);
if (this.seekTime < 0) {
// The mousemove fires for 10+px out to the left
this.seekTime = 0;
}
if (this.seekTime > this.player.media.duration - 1) {
// Took 1 second off the duration for safety, because different players can disagree on the real duration of a video
this.seekTime = this.player.media.duration - 1;
}
this.mousePosX = event.pageX;
// Set time text inside image container
this.elements.thumb.time.innerText = formatTime(this.seekTime);
// Get marker point for time
const point = this.player.config.markers?.points?.find(({ time: t }) => t === Math.round(this.seekTime));
// Append the point label to the tooltip
if (point) {
// this.elements.thumb.time.innerText.concat('\n');
this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);
}
}
// Download and show image
this.showImageAtCurrentTime();
};
endMove = () => {
this.toggleThumbContainer(false, true);
};
startScrubbing = (event) => {
// Only act on left mouse button (0), or touch device (event.button does not exist or is false)
if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {
this.mouseDown = true;
// Wait until media has a duration
if (this.player.media.duration) {
this.toggleScrubbingContainer(true);
this.toggleThumbContainer(false, true);
// Download and show image
this.showImageAtCurrentTime();
}
}
};
endScrubbing = () => {
this.mouseDown = false;
// Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview
if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {
// The video was already seeked/loaded at the chosen time - hide immediately
this.toggleScrubbingContainer(false);
} else {
// The video hasn't seeked yet. Wait for that
once.call(this.player, this.player.media, 'timeupdate', () => {
// Re-check mousedown - we might have already started scrubbing again
if (!this.mouseDown) {
this.toggleScrubbingContainer(false);
}
});
}
};
/**
* Setup hooks for Plyr and window events
*/
listeners = () => {
// Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering
this.player.on('play', () => {
this.toggleThumbContainer(false, true);
});
this.player.on('seeked', () => {
this.toggleThumbContainer(false);
});
this.player.on('timeupdate', () => {
this.lastTime = this.player.media.currentTime;
});
};
/**
* Create HTML elements for image containers
*/
render = () => {
// Create HTML element: plyr__preview-thumbnail-container
this.elements.thumb.container = createElement('div', {
class: this.player.config.classNames.previewThumbnails.thumbContainer,
});
// Wrapper for the image for styling
this.elements.thumb.imageContainer = createElement('div', {
class: this.player.config.classNames.previewThumbnails.imageContainer,
});
this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);
// Create HTML element, parent+span: time text (e.g., 01:32:00)
const timeContainer = createElement('div', {
class: this.player.config.classNames.previewThumbnails.timeContainer,
});
this.elements.thumb.time = createElement('span', {}, '00:00');
timeContainer.appendChild(this.elements.thumb.time);
this.elements.thumb.imageContainer.appendChild(timeContainer);
// Inject the whole thumb
if (is.element(this.player.elements.progress)) {
this.player.elements.progress.appendChild(this.elements.thumb.container);
}
// Create HTML element: plyr__preview-scrubbing-container
this.elements.scrubbing.container = createElement('div', {
class: this.player.config.classNames.previewThumbnails.scrubbingContainer,
});
this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);
};
destroy = () => {
if (this.elements.thumb.container) {
this.elements.thumb.container.remove();
}
if (this.elements.scrubbing.container) {
this.elements.scrubbing.container.remove();
}
};
showImageAtCurrentTime = () => {
if (this.mouseDown) {
this.setScrubbingContainerSize();
} else {
this.setThumbContainerSizeAndPos();
}
// Find the desired thumbnail index
// TODO: Handle a video longer than the thumbs where thumbNum is null
const thumbNum = this.thumbnails[0].frames.findIndex(
(frame) => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,
);
const hasThumb = thumbNum >= 0;
let qualityIndex = 0;
// Show the thumb container if we're not scrubbing
if (!this.mouseDown) {
this.toggleThumbContainer(hasThumb);
}
// No matching thumb found
if (!hasThumb) {
return;
}
// Check to see if we've already downloaded higher quality versions of this image
this.thumbnails.forEach((thumbnail, index) => {
if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {
qualityIndex = index;
}
});
// Only proceed if either thumb num or thumbfilename has changed
if (thumbNum !== this.showingThumb) {
this.showingThumb = thumbNum;
this.loadImage(qualityIndex);
}
};
// Show the image that's currently specified in this.showingThumb
loadImage = (qualityIndex = 0) => {
const thumbNum = this.showingThumb;
const thumbnail = this.thumbnails[qualityIndex];
const { urlPrefix } = thumbnail;
const frame = thumbnail.frames[thumbNum];
const thumbFilename = thumbnail.frames[thumbNum].text;
const thumbUrl = urlPrefix + thumbFilename;
if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {
// If we're already loading a previous image, remove its onload handler - we don't want it to load after this one
// Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort
if (this.loadingImage && this.usingSprites) {
this.loadingImage.onload = null;
}
// We're building and adding a new image. In other implementations of similar functionality (YouTube), background image
// is instead used. But this causes issues with larger images in Firefox and Safari - switching between background
// images causes a flicker. Putting a new image over the top does not
const previewImage = new Image();
previewImage.src = thumbUrl;
previewImage.dataset.index = thumbNum;
previewImage.dataset.filename = thumbFilename;
this.showingThumbFilename = thumbFilename;
this.player.debug.log(`Loading image: ${thumbUrl}`);
// For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...
previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);
this.loadingImage = previewImage;
this.removeOldImages(previewImage);
} else {
// Update the existing image
this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);
this.currentImageElement.dataset.index = thumbNum;
this.removeOldImages(this.currentImageElement);
}
};
showImage = (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {
this.player.debug.log(
`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`,
);
this.setImageSizeAndOffset(previewImage, frame);
if (newImage) {
this.currentImageContainer.appendChild(previewImage);
this.currentImageElement = previewImage;
if (!this.loadedImages.includes(thumbFilename)) {
this.loadedImages.push(thumbFilename);
}
}
// Preload images before and after the current one
// Show higher quality of the same frame
// Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading
this.preloadNearby(thumbNum, true)
.then(this.preloadNearby(thumbNum, false))
.then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));
};
// Remove all preview images that aren't the designated current image
removeOldImages = (currentImage) => {
// Get a list of all images, convert it from a DOM list to an array
Array.from(this.currentImageContainer.children).forEach((image) => {
if (image.tagName.toLowerCase() !== 'img') {
return;
}
const removeDelay = this.usingSprites ? 500 : 1000;
if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {
// Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients
// First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function
// eslint-disable-next-line no-param-reassign
image.dataset.deleting = true;
// This has to be set before the timeout - to prevent issues switching between hover and scrub
const { currentImageContainer } = this;
setTimeout(() => {
currentImageContainer.removeChild(image);
this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);
}, removeDelay);
}
});
};
// Preload images before and after the current one. Only if the user is still hovering/seeking the same frame
// This will only preload the lowest quality
preloadNearby = (thumbNum, forward = true) => {
return new Promise((resolve) => {
setTimeout(() => {
const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;
if (this.showingThumbFilename === oldThumbFilename) {
// Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away
let thumbnailsClone;
if (forward) {
thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);
} else {
thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();
}
let foundOne = false;
thumbnailsClone.forEach((frame) => {
const newThumbFilename = frame.text;
if (newThumbFilename !== oldThumbFilename) {
// Found one with a different filename. Make sure it hasn't already been loaded on this page visit
if (!this.loadedImages.includes(newThumbFilename)) {
foundOne = true;
this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);
const { urlPrefix } = this.thumbnails[0];
const thumbURL = urlPrefix + newThumbFilename;
const previewImage = new Image();
previewImage.src = thumbURL;
previewImage.onload = () => {
this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);
if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);
// We don't resolve until the thumb is loaded
resolve();
};
}
}
});
// If there are none to preload then we want to resolve immediately
if (!foundOne) {
resolve();
}
}
}, 300);
});
};
// If user has been hovering current image for half a second, look for a higher quality one
getHigherQuality = (currentQualityIndex, previewImage, frame, thumbFilename) => {
if (currentQualityIndex < this.thumbnails.length - 1) {
// Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container
let previewImageHeight = previewImage.naturalHeight;
if (this.usingSprites) {
previewImageHeight = frame.h;
}
if (previewImageHeight < this.thumbContainerHeight) {
// Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while
setTimeout(() => {
// Make sure the mouse hasn't already moved on and started hovering at another image
if (this.showingThumbFilename === thumbFilename) {
this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);
this.loadImage(currentQualityIndex + 1);
}
}, 300);
}
}
};
get currentImageContainer() {
return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;
}
get usingSprites() {
return Object.keys(this.thumbnails[0].frames[0]).includes('w');
}
get thumbAspectRatio() {
if (this.usingSprites) {
return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;
}
return this.thumbnails[0].width / this.thumbnails[0].height;
}
get thumbContainerHeight() {
if (this.mouseDown) {
const { height } = fitRatio(this.thumbAspectRatio, {
width: this.player.media.clientWidth,
height: this.player.media.clientHeight,
});
return height;
}
// If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)
if (this.sizeSpecifiedInCSS) {
return this.elements.thumb.imageContainer.clientHeight;
}
return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);
}
get currentImageElement() {
return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;
}
set currentImageElement(element) {
if (this.mouseDown) {
this.currentScrubbingImageElement = element;
} else {
this.currentThumbnailImageElement = element;
}
}
toggleThumbContainer = (toggle = false, clearShowing = false) => {
const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;
this.elements.thumb.container.classList.toggle(className, toggle);
if (!toggle && clearShowing) {
this.showingThumb = null;
this.showingThumbFilename = null;
}
};
toggleScrubbingContainer = (toggle = false) => {
const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;
this.elements.scrubbing.container.classList.toggle(className, toggle);
if (!toggle) {
this.showingThumb = null;
this.showingThumbFilename = null;
}
};
determineContainerAutoSizing = () => {
if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {
// This will prevent auto sizing in this.setThumbContainerSizeAndPos()
this.sizeSpecifiedInCSS = true;
}
};
// Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS
setThumbContainerSizeAndPos = () => {
const { imageContainer } = this.elements.thumb;
if (!this.sizeSpecifiedInCSS) {
const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);
imageContainer.style.height = `${this.thumbContainerHeight}px`;
imageContainer.style.width = `${thumbWidth}px`;
} else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {
const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);
imageContainer.style.width = `${thumbWidth}px`;
} else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {
const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);
imageContainer.style.height = `${thumbHeight}px`;
}
this.setThumbContainerPos();
};
setThumbContainerPos = () => {
const scrubberRect = this.player.elements.progress.getBoundingClientRect();
const containerRect = this.player.elements.container.getBoundingClientRect();
const { container } = this.elements.thumb;
// Find the lowest and highest desired left-position, so we don't slide out the side of the video container
const min = containerRect.left - scrubberRect.left + 10;
const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;
// Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth
const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;
const clamped = clamp(position, min, max);
// Move the popover position
container.style.left = `${clamped}px`;
// The arrow can follow the cursor
container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);
};
// Can't use 100% width, in case the video is a different aspect ratio to the video container
setScrubbingContainerSize = () => {
const { width, height } = fitRatio(this.thumbAspectRatio, {
width: this.player.media.clientWidth,
height: this.player.media.clientHeight,
});
this.elements.scrubbing.container.style.width = `${width}px`;
this.elements.scrubbing.container.style.height = `${height}px`;
};
// Sprites need to be offset to the correct location
setImageSizeAndOffset = (previewImage, frame) => {
if (!this.usingSprites) return;
// Find difference between height and preview container height
const multiplier = this.thumbContainerHeight / frame.h;
// eslint-disable-next-line no-param-reassign
previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;
// eslint-disable-next-line no-param-reassign
previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;
// eslint-disable-next-line no-param-reassign
previewImage.style.left = `-${frame.x * multiplier}px`;
// eslint-disable-next-line no-param-reassign
previewImage.style.top = `-${frame.y * multiplier}px`;
};
}
export default PreviewThumbnails;
-639
View File
@@ -1,639 +0,0 @@
import { createElement } from '../utils/elements';
import { once } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import { formatTime } from '../utils/time';
// Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg"
const parseVtt = vttDataString => {
const processedList = [];
const frames = vttDataString.split(/\r\n\r\n|\n\n|\r\r/);
frames.forEach(frame => {
const result = {};
const lines = frame.split(/\r\n|\n|\r/);
lines.forEach(line => {
if (!is.number(result.startTime)) {
// The line with start and end times on it is the first line of interest
const matchTimes = line.match(
/([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,
); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT
if (matchTimes) {
result.startTime =
Number(matchTimes[1]) * 60 * 60 +
Number(matchTimes[2]) * 60 +
Number(matchTimes[3]) +
Number(`0.${matchTimes[4]}`);
result.endTime =
Number(matchTimes[6]) * 60 * 60 +
Number(matchTimes[7]) * 60 +
Number(matchTimes[8]) +
Number(`0.${matchTimes[9]}`);
}
} else if (!is.empty(line.trim()) && is.empty(result.text)) {
// If we already have the startTime, then we're definitely up to the text line(s)
const lineSplit = line.trim().split('#xywh=');
[result.text] = lineSplit;
// If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image
if (lineSplit[1]) {
[result.x, result.y, result.w, result.h] = lineSplit[1].split(',');
}
}
});
if (result.text) {
processedList.push(result);
}
});
return processedList;
};
/**
* Preview thumbnails for seek hover and scrubbing
* Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar
* Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed
*
* Notes:
* - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole
* - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails
* - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered
*/
class PreviewThumbnails {
/**
* PreviewThumbnails constructor.
* @param {Plyr} player
* @return {PreviewThumbnails}
*/
constructor(player) {
this.player = player;
this.thumbnails = [];
this.loaded = false;
this.lastMouseMoveTime = Date.now();
this.mouseDown = false;
this.loadedImages = [];
this.elements = {
thumb: {},
scrubbing: {},
};
this.load();
}
get enabled() {
return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;
}
load() {
// Togglethe regular seek tooltip
if (this.player.elements.display.seekTooltip) {
this.player.elements.display.seekTooltip.hidden = this.enabled;
}
if (!this.enabled) {
return;
}
this.getThumbnails().then(() => {
// Render DOM elements
this.render();
// Check to see if thumb container size was specified manually in CSS
this.determineContainerAutoSizing();
this.loaded = true;
});
}
// Download VTT files and parse them
getThumbnails() {
return new Promise(resolve => {
const { src } = this.player.config.previewThumbnails;
if (is.empty(src)) {
throw new Error('Missing previewThumbnails.src config attribute');
}
// If string, convert into single-element list
const urls = is.string(src) ? [src] : src;
// Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
const promises = urls.map(u => this.getThumbnail(u));
Promise.all(promises).then(() => {
// Sort smallest to biggest (e.g., [120p, 480p, 1080p])
this.thumbnails.sort((x, y) => x.height - y.height);
this.player.debug.log('Preview thumbnails', this.thumbnails);
resolve();
});
});
}
// Process individual VTT file
getThumbnail(url) {
return new Promise(resolve => {
fetch(url).then(response => {
const thumbnail = {
frames: parseVtt(response),
height: null,
urlPrefix: '',
};
// If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file
// If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank
if (!thumbnail.frames[0].text.startsWith('/')) {
thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
}
// Download the first frame, so that we can determine/set the height of this thumbnailsDef
const tempImage = new Image();
tempImage.onload = () => {
thumbnail.height = tempImage.naturalHeight;
thumbnail.width = tempImage.naturalWidth;
this.thumbnails.push(thumbnail);
resolve();
};
tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;
});
});
}
startMove(event) {
if (!this.loaded) {
return;
}
if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) {
return;
}
// Wait until media has a duration
if (!this.player.media.duration) {
return;
}
if (event.type === 'touchmove') {
// Calculate seek hover position as approx video seconds
this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);
} else {
// Calculate seek hover position as approx video seconds
const clientRect = this.player.elements.progress.getBoundingClientRect();
const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left);
this.seekTime = this.player.media.duration * (percentage / 100);
if (this.seekTime < 0) {
// The mousemove fires for 10+px out to the left
this.seekTime = 0;
}
if (this.seekTime > this.player.media.duration - 1) {
// Took 1 second off the duration for safety, because different players can disagree on the real duration of a video
this.seekTime = this.player.media.duration - 1;
}
this.mousePosX = event.pageX;
// Set time text inside image container
this.elements.thumb.time.innerText = formatTime(this.seekTime);
}
// Download and show image
this.showImageAtCurrentTime();
}
endMove() {
this.toggleThumbContainer(false, true);
}
startScrubbing(event) {
// Only act on left mouse button (0), or touch device (event.button is false)
if (event.button === false || event.button === 0) {
this.mouseDown = true;
// Wait until media has a duration
if (this.player.media.duration) {
this.toggleScrubbingContainer(true);
this.toggleThumbContainer(false, true);
// Download and show image
this.showImageAtCurrentTime();
}
}
}
endScrubbing() {
this.mouseDown = false;
// Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview
if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {
// The video was already seeked/loaded at the chosen time - hide immediately
this.toggleScrubbingContainer(false);
} else {
// The video hasn't seeked yet. Wait for that
once.call(this.player, this.player.media, 'timeupdate', () => {
// Re-check mousedown - we might have already started scrubbing again
if (!this.mouseDown) {
this.toggleScrubbingContainer(false);
}
});
}
}
/**
* Setup hooks for Plyr and window events
*/
listeners() {
// Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering
this.player.on('play', () => {
this.toggleThumbContainer(false, true);
});
this.player.on('seeked', () => {
this.toggleThumbContainer(false);
});
this.player.on('timeupdate', () => {
this.lastTime = this.player.media.currentTime;
});
}
/**
* Create HTML elements for image containers
*/
render() {
// Create HTML element: plyr__preview-thumbnail-container
this.elements.thumb.container = createElement('div', {
class: this.player.config.classNames.previewThumbnails.thumbContainer,
});
// Wrapper for the image for styling
this.elements.thumb.imageContainer = createElement('div', {
class: this.player.config.classNames.previewThumbnails.imageContainer,
});
this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);
// Create HTML element, parent+span: time text (e.g., 01:32:00)
const timeContainer = createElement('div', {
class: this.player.config.classNames.previewThumbnails.timeContainer,
});
this.elements.thumb.time = createElement('span', {}, '00:00');
timeContainer.appendChild(this.elements.thumb.time);
this.elements.thumb.container.appendChild(timeContainer);
// Inject the whole thumb
this.player.elements.progress.appendChild(this.elements.thumb.container);
// Create HTML element: plyr__preview-scrubbing-container
this.elements.scrubbing.container = createElement('div', {
class: this.player.config.classNames.previewThumbnails.scrubbingContainer,
});
this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);
}
showImageAtCurrentTime() {
if (this.mouseDown) {
this.setScrubbingContainerSize();
} else {
this.setThumbContainerSizeAndPos();
}
// Find the desired thumbnail index
// TODO: Handle a video longer than the thumbs where thumbNum is null
const thumbNum = this.thumbnails[0].frames.findIndex(
frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,
);
const hasThumb = thumbNum >= 0;
let qualityIndex = 0;
// Show the thumb container if we're not scrubbing
if (!this.mouseDown) {
this.toggleThumbContainer(hasThumb);
}
// No matching thumb found
if (!hasThumb) {
return;
}
// Check to see if we've already downloaded higher quality versions of this image
this.thumbnails.forEach((thumbnail, index) => {
if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {
qualityIndex = index;
}
});
// Only proceed if either thumbnum or thumbfilename has changed
if (thumbNum !== this.showingThumb) {
this.showingThumb = thumbNum;
this.loadImage(qualityIndex);
}
}
// Show the image that's currently specified in this.showingThumb
loadImage(qualityIndex = 0) {
const thumbNum = this.showingThumb;
const thumbnail = this.thumbnails[qualityIndex];
const { urlPrefix } = thumbnail;
const frame = thumbnail.frames[thumbNum];
const thumbFilename = thumbnail.frames[thumbNum].text;
const thumbUrl = urlPrefix + thumbFilename;
if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {
// If we're already loading a previous image, remove its onload handler - we don't want it to load after this one
// Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort
if (this.loadingImage && this.usingSprites) {
this.loadingImage.onload = null;
}
// We're building and adding a new image. In other implementations of similar functionality (YouTube), background image
// is instead used. But this causes issues with larger images in Firefox and Safari - switching between background
// images causes a flicker. Putting a new image over the top does not
const previewImage = new Image();
previewImage.src = thumbUrl;
previewImage.dataset.index = thumbNum;
previewImage.dataset.filename = thumbFilename;
this.showingThumbFilename = thumbFilename;
this.player.debug.log(`Loading image: ${thumbUrl}`);
// For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...
previewImage.onload = () =>
this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);
this.loadingImage = previewImage;
this.removeOldImages(previewImage);
} else {
// Update the existing image
this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);
this.currentImageElement.dataset.index = thumbNum;
this.removeOldImages(this.currentImageElement);
}
}
showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) {
this.player.debug.log(
`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`,
);
this.setImageSizeAndOffset(previewImage, frame);
if (newImage) {
this.currentImageContainer.appendChild(previewImage);
this.currentImageElement = previewImage;
if (!this.loadedImages.includes(thumbFilename)) {
this.loadedImages.push(thumbFilename);
}
}
// Preload images before and after the current one
// Show higher quality of the same frame
// Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading
this.preloadNearby(thumbNum, true)
.then(this.preloadNearby(thumbNum, false))
.then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));
}
// Remove all preview images that aren't the designated current image
removeOldImages(currentImage) {
// Get a list of all images, convert it from a DOM list to an array
Array.from(this.currentImageContainer.children).forEach(image => {
if (image.tagName.toLowerCase() !== 'img') {
return;
}
const removeDelay = this.usingSprites ? 500 : 1000;
if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {
// Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients
// First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function
image.dataset.deleting = true;
// This has to be set before the timeout - to prevent issues switching between hover and scrub
const { currentImageContainer } = this;
setTimeout(() => {
currentImageContainer.removeChild(image);
this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);
}, removeDelay);
}
});
}
// Preload images before and after the current one. Only if the user is still hovering/seeking the same frame
// This will only preload the lowest quality
preloadNearby(thumbNum, forward = true) {
return new Promise(resolve => {
setTimeout(() => {
const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;
if (this.showingThumbFilename === oldThumbFilename) {
// Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away
let thumbnailsClone;
if (forward) {
thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);
} else {
thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();
}
let foundOne = false;
thumbnailsClone.forEach(frame => {
const newThumbFilename = frame.text;
if (newThumbFilename !== oldThumbFilename) {
// Found one with a different filename. Make sure it hasn't already been loaded on this page visit
if (!this.loadedImages.includes(newThumbFilename)) {
foundOne = true;
this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);
const { urlPrefix } = this.thumbnails[0];
const thumbURL = urlPrefix + newThumbFilename;
const previewImage = new Image();
previewImage.src = thumbURL;
previewImage.onload = () => {
this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);
if (!this.loadedImages.includes(newThumbFilename))
this.loadedImages.push(newThumbFilename);
// We don't resolve until the thumb is loaded
resolve();
};
}
}
});
// If there are none to preload then we want to resolve immediately
if (!foundOne) {
resolve();
}
}
}, 300);
});
}
// If user has been hovering current image for half a second, look for a higher quality one
getHigherQuality(currentQualityIndex, previewImage, frame, thumbFilename) {
if (currentQualityIndex < this.thumbnails.length - 1) {
// Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container
let previewImageHeight = previewImage.naturalHeight;
if (this.usingSprites) {
previewImageHeight = frame.h;
}
if (previewImageHeight < this.thumbContainerHeight) {
// Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while
setTimeout(() => {
// Make sure the mouse hasn't already moved on and started hovering at another image
if (this.showingThumbFilename === thumbFilename) {
this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);
this.loadImage(currentQualityIndex + 1);
}
}, 300);
}
}
}
get currentImageContainer() {
if (this.mouseDown) {
return this.elements.scrubbing.container;
}
return this.elements.thumb.imageContainer;
}
get usingSprites() {
return Object.keys(this.thumbnails[0].frames[0]).includes('w');
}
get thumbAspectRatio() {
if (this.usingSprites) {
return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;
}
return this.thumbnails[0].width / this.thumbnails[0].height;
}
get thumbContainerHeight() {
if (this.mouseDown) {
// Can't use media.clientHeight - HTML5 video goes big and does black bars above and below
return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio);
}
return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);
}
get currentImageElement() {
if (this.mouseDown) {
return this.currentScrubbingImageElement;
}
return this.currentThumbnailImageElement;
}
set currentImageElement(element) {
if (this.mouseDown) {
this.currentScrubbingImageElement = element;
} else {
this.currentThumbnailImageElement = element;
}
}
toggleThumbContainer(toggle = false, clearShowing = false) {
const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;
this.elements.thumb.container.classList.toggle(className, toggle);
if (!toggle && clearShowing) {
this.showingThumb = null;
this.showingThumbFilename = null;
}
}
toggleScrubbingContainer(toggle = false) {
const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;
this.elements.scrubbing.container.classList.toggle(className, toggle);
if (!toggle) {
this.showingThumb = null;
this.showingThumbFilename = null;
}
}
determineContainerAutoSizing() {
if (this.elements.thumb.imageContainer.clientHeight > 20) {
// This will prevent auto sizing in this.setThumbContainerSizeAndPos()
this.sizeSpecifiedInCSS = true;
}
}
// Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS
setThumbContainerSizeAndPos() {
if (!this.sizeSpecifiedInCSS) {
const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);
this.elements.thumb.imageContainer.style.height = `${this.thumbContainerHeight}px`;
this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`;
}
this.setThumbContainerPos();
}
setThumbContainerPos() {
const seekbarRect = this.player.elements.progress.getBoundingClientRect();
const plyrRect = this.player.elements.container.getBoundingClientRect();
const { container } = this.elements.thumb;
// Find the lowest and highest desired left-position, so we don't slide out the side of the video container
const minVal = plyrRect.left - seekbarRect.left + 10;
const maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10;
// Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth
let previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2;
if (previewPos < minVal) {
previewPos = minVal;
}
if (previewPos > maxVal) {
previewPos = maxVal;
}
container.style.left = `${previewPos}px`;
}
// Can't use 100% width, in case the video is a different aspect ratio to the video container
setScrubbingContainerSize() {
this.elements.scrubbing.container.style.width = `${this.player.media.clientWidth}px`;
// Can't use media.clientHeight - html5 video goes big and does black bars above and below
this.elements.scrubbing.container.style.height = `${this.player.media.clientWidth / this.thumbAspectRatio}px`;
}
// Sprites need to be offset to the correct location
setImageSizeAndOffset(previewImage, frame) {
if (!this.usingSprites) {
return;
}
// Find difference between height and preview container height
const multiplier = this.thumbContainerHeight / frame.h;
previewImage.style.height = `${Math.floor(previewImage.naturalHeight * multiplier)}px`;
previewImage.style.width = `${Math.floor(previewImage.naturalWidth * multiplier)}px`;
previewImage.style.left = `-${frame.x * multiplier}px`;
previewImage.style.top = `-${frame.y * multiplier}px`;
}
}
export default PreviewThumbnails;
+365 -321
View File
@@ -9,386 +9,430 @@ 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 { extend } from '../utils/objects';
import loadScript from '../utils/load-script';
import { format, stripHTML } from '../utils/strings';
import { setAspectRatio } from '../utils/style';
import { roundAspectRatio, setAspectRatio } from '../utils/style';
import { buildUrlParams } from '../utils/urls';
// Parse Vimeo ID from URL
function parseId(url) {
if (is.empty(url)) {
return null;
}
if (is.empty(url)) {
return null;
}
if (is.number(Number(url))) {
return url;
}
if (is.number(Number(url))) {
return url;
}
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
return url.match(regex) ? RegExp.$2 : url;
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
return url.match(regex) ? RegExp.$2 : url;
}
// Try to extract a hash for private videos from the URL
function parseHash(url) {
/* This regex matches a hexadecimal hash if given in any of these forms:
* - [https://player.]vimeo.com/video/{id}/{hash}[?params]
* - [https://player.]vimeo.com/video/{id}?h={hash}[&params]
* - [https://player.]vimeo.com/video/{id}?[params]&h={hash}
* - video/{id}/{hash}
* If matched, the hash is available in capture group 4
*/
const regex = /^.*(vimeo.com\/|video\/)(\d+)(\?.*&*h=|\/)+([\d,a-f]+)/;
const found = url.match(regex);
return found && found.length === 5 ? found[4] : null;
}
// Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) {
this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) {
this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
const vimeo = {
setup() {
// Add embed class for responsive
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
setup() {
const player = this;
// Set intial ratio
setAspectRatio.call(this);
// Add embed class for responsive
toggleClass(player.elements.wrapper, player.config.classNames.embed, true);
// Load the API if not already
if (!is.object(window.Vimeo)) {
loadScript(this.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(this);
})
.catch(error => {
this.debug.warn('Vimeo API failed to load', error);
});
} else {
vimeo.ready.call(this);
}
},
// Set speed options from config
player.options.speed = player.config.speed.options;
// API Ready
ready() {
const player = this;
const config = player.config.vimeo;
// Set intial ratio
setAspectRatio.call(player);
// Get Vimeo params for the iframe
const params = buildUrlParams(
extend(
{},
{
loop: player.config.loop.active,
autoplay: player.autoplay,
muted: player.muted,
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
},
config,
),
);
// Load the SDK if not already
if (!is.object(window.Vimeo)) {
loadScript(player.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(player);
})
.catch((error) => {
player.debug.warn('Vimeo SDK (player.js) failed to load', error);
});
} else {
vimeo.ready.call(player);
}
},
// Get the source URL or ID
let source = player.media.getAttribute('src');
// API Ready
ready() {
const player = this;
const config = player.config.vimeo;
const { premium, referrerPolicy, ...frameParams } = config;
// Get the source URL or ID
let source = player.media.getAttribute('src');
let hash = '';
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(player.config.attributes.embed.id);
// hash can also be set as attribute on the <div>
hash = player.media.getAttribute(player.config.attributes.embed.hash);
} else {
hash = parseHash(source);
}
const hashParam = hash ? { h: hash } : {};
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(player.config.attributes.embed.id);
// If the owner has a pro or premium account then we can hide controls etc
if (premium) {
Object.assign(frameParams, {
controls: false,
sidedock: false,
});
}
// Get Vimeo params for the iframe
const params = buildUrlParams({
loop: player.config.loop.active,
autoplay: player.autoplay,
muted: player.muted,
gesture: 'media',
playsinline: player.config.playsinline,
// hash has to be added to iframe-URL
...hashParam,
...frameParams,
});
const id = parseId(source);
// Build an iframe
const iframe = createElement('iframe');
const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute(
'allow',
['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '),
);
// Set the referrer policy if required
if (!is.empty(referrerPolicy)) {
iframe.setAttribute('referrerPolicy', referrerPolicy);
}
// Inject the package
if (premium || !config.customControls) {
iframe.setAttribute('data-poster', player.poster);
player.media = replaceElement(iframe, player.media);
} else {
const wrapper = createElement('div', {
class: player.config.classNames.embedContainer,
'data-poster': player.poster,
});
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
}
// Get poster image
if (!config.customControls) {
fetch(format(player.config.urls.vimeo.api, src)).then((response) => {
if (is.empty(response) || !response.thumbnail_url) {
return;
}
const id = parseId(source);
// Set and show poster
ui.setPoster.call(player, response.thumbnail_url).catch(() => {});
});
}
// Build an iframe
const iframe = createElement('iframe');
const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay');
// Setup instance
// https://github.com/vimeo/player.js
player.embed = new window.Vimeo.Player(iframe, {
autopause: player.config.autopause,
muted: player.muted,
});
// Get poster, if already set
const { poster } = player;
player.media.paused = true;
player.media.currentTime = 0;
// Inject the package
const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
// Disable native text track rendering
if (player.supported.ui) {
player.embed.disableTextTrack();
}
// Get poster image
fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (is.empty(response)) {
return;
}
// Create a faux HTML5 API using the Vimeo API
player.media.play = () => {
assurePlaybackState.call(player, true);
return player.embed.play();
};
// Get the URL for thumbnail
const url = new URL(response[0].thumbnail_large);
player.media.pause = () => {
assurePlaybackState.call(player, false);
return player.embed.pause();
};
// Get original image
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
player.media.stop = () => {
player.pause();
player.currentTime = 0;
};
// Set and show poster
ui.setPoster.call(player, url.href).catch(() => {});
});
// Seeking
let { currentTime } = player.media;
Object.defineProperty(player.media, 'currentTime', {
get() {
return currentTime;
},
set(time) {
// Vimeo will automatically play on seek if the video hasn't been played before
// Setup instance
// https://github.com/vimeo/player.js
player.embed = new window.Vimeo.Player(iframe, {
autopause: player.config.autopause,
muted: player.muted,
});
// Get current paused state and volume etc
const { embed, media, paused, volume } = player;
const restorePause = paused && !embed.hasPlayed;
player.media.paused = true;
player.media.currentTime = 0;
// Set seeking state and trigger event
media.seeking = true;
triggerEvent.call(player, media, 'seeking');
// Disable native text track rendering
if (player.supported.ui) {
player.embed.disableTextTrack();
}
// If paused, mute until seek is complete
Promise.resolve(restorePause && embed.setVolume(0))
// Seek
.then(() => embed.setCurrentTime(time))
// Restore paused
.then(() => restorePause && embed.pause())
// Restore volume
.then(() => restorePause && embed.setVolume(volume))
.catch(() => {
// Do nothing
});
},
});
// Create a faux HTML5 API using the Vimeo API
player.media.play = () => {
assurePlaybackState.call(player, true);
return player.embed.play();
};
player.media.pause = () => {
assurePlaybackState.call(player, false);
return player.embed.pause();
};
player.media.stop = () => {
player.pause();
player.currentTime = 0;
};
// Seeking
let { currentTime } = player.media;
Object.defineProperty(player.media, 'currentTime', {
get() {
return currentTime;
},
set(time) {
// Vimeo will automatically play on seek if the video hasn't been played before
// Get current paused state and volume etc
const { embed, media, paused, volume } = player;
const restorePause = paused && !embed.hasPlayed;
// Set seeking state and trigger event
media.seeking = true;
triggerEvent.call(player, media, 'seeking');
// If paused, mute until seek is complete
Promise.resolve(restorePause && embed.setVolume(0))
// Seek
.then(() => embed.setCurrentTime(time))
// Restore paused
.then(() => restorePause && embed.pause())
// Restore volume
.then(() => restorePause && embed.setVolume(volume))
.catch(() => {
// Do nothing
});
},
});
// Playback speed
let speed = player.config.speed.selected;
Object.defineProperty(player.media, 'playbackRate', {
get() {
return speed;
},
set(input) {
player.embed
.setPlaybackRate(input)
.then(() => {
speed = input;
triggerEvent.call(player, player.media, 'ratechange');
})
.catch(error => {
// Hide menu item (and menu if empty)
if (error.name === 'Error') {
controls.setSpeedMenu.call(player, []);
}
});
},
});
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
get() {
return volume;
},
set(input) {
player.embed.setVolume(input).then(() => {
volume = input;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Muted
let { muted } = player.config;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : false;
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
muted = toggle;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Loop
let { loop } = player.config;
Object.defineProperty(player.media, 'loop', {
get() {
return loop;
},
set(input) {
const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => {
loop = toggle;
});
},
});
// Source
let currentSrc;
// Playback speed
let speed = player.config.speed.selected;
Object.defineProperty(player.media, 'playbackRate', {
get() {
return speed;
},
set(input) {
player.embed
.getVideoUrl()
.then(value => {
currentSrc = value;
controls.setDownloadLink.call(player);
})
.catch(error => {
this.debug.warn(error);
});
.setPlaybackRate(input)
.then(() => {
speed = input;
triggerEvent.call(player, player.media, 'ratechange');
})
.catch(() => {
// Cannot set Playback Rate, Video is probably not on Pro account
player.options.speed = [1];
});
},
});
Object.defineProperty(player.media, 'currentSrc', {
get() {
return currentSrc;
},
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
get() {
return volume;
},
set(input) {
player.embed.setVolume(input).then(() => {
volume = input;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Ended
Object.defineProperty(player.media, 'ended', {
get() {
return player.currentTime === player.duration;
},
// Muted
let { muted } = player.config;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : false;
player.embed.setMuted(toggle ? true : player.config.muted).then(() => {
muted = toggle;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Set aspect ratio based on video size
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
const [width, height] = dimensions;
player.embed.ratio = `${width}:${height}`;
setAspectRatio.call(this, player.embed.ratio);
// Loop
let { loop } = player.config;
Object.defineProperty(player.media, 'loop', {
get() {
return loop;
},
set(input) {
const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => {
loop = toggle;
});
},
});
// Set autopause
player.embed.setAutopause(player.config.autopause).then(state => {
player.config.autopause = state;
});
// Source
let currentSrc;
player.embed
.getVideoUrl()
.then((value) => {
currentSrc = value;
controls.setDownloadUrl.call(player);
})
.catch((error) => {
this.debug.warn(error);
});
// Get title
player.embed.getVideoTitle().then(title => {
player.config.title = title;
ui.setTitle.call(this);
});
Object.defineProperty(player.media, 'currentSrc', {
get() {
return currentSrc;
},
});
// Get current time
player.embed.getCurrentTime().then(value => {
currentTime = value;
triggerEvent.call(player, player.media, 'timeupdate');
});
// Ended
Object.defineProperty(player.media, 'ended', {
get() {
return player.currentTime === player.duration;
},
});
// Get duration
player.embed.getDuration().then(value => {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
});
// Set aspect ratio based on video size
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then((dimensions) => {
const [width, height] = dimensions;
player.embed.ratio = roundAspectRatio(width, height);
setAspectRatio.call(this);
});
// Get captions
player.embed.getTextTracks().then(tracks => {
player.media.textTracks = tracks;
captions.setup.call(player);
});
// Set autopause
player.embed.setAutopause(player.config.autopause).then((state) => {
player.config.autopause = state;
});
player.embed.on('cuechange', ({ cues = [] }) => {
const strippedCues = cues.map(cue => stripHTML(cue.text));
captions.updateCues.call(player, strippedCues);
});
// Get title
player.embed.getVideoTitle().then((title) => {
player.config.title = title;
ui.setTitle.call(this);
});
player.embed.on('loaded', () => {
// Assure state and events are updated on autoplay
player.embed.getPaused().then(paused => {
assurePlaybackState.call(player, !paused);
if (!paused) {
triggerEvent.call(player, player.media, 'playing');
}
});
// Get current time
player.embed.getCurrentTime().then((value) => {
currentTime = value;
triggerEvent.call(player, player.media, 'timeupdate');
});
if (is.element(player.embed.element) && player.supported.ui) {
const frame = player.embed.element;
// Get duration
player.embed.getDuration().then((value) => {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
});
// Fix keyboard focus issues
// https://github.com/sampotts/plyr/issues/317
frame.setAttribute('tabindex', -1);
}
});
// Get captions
player.embed.getTextTracks().then((tracks) => {
player.media.textTracks = tracks;
captions.setup.call(player);
});
player.embed.on('play', () => {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('cuechange', ({ cues = [] }) => {
const strippedCues = cues.map((cue) => stripHTML(cue.text));
captions.updateCues.call(player, strippedCues);
});
player.embed.on('pause', () => {
assurePlaybackState.call(player, false);
});
player.embed.on('loaded', () => {
// Assure state and events are updated on autoplay
player.embed.getPaused().then((paused) => {
assurePlaybackState.call(player, !paused);
if (!paused) {
triggerEvent.call(player, player.media, 'playing');
}
});
player.embed.on('timeupdate', data => {
player.media.seeking = false;
currentTime = data.seconds;
triggerEvent.call(player, player.media, 'timeupdate');
});
if (is.element(player.embed.element) && player.supported.ui) {
const frame = player.embed.element;
player.embed.on('progress', data => {
player.media.buffered = data.percent;
triggerEvent.call(player, player.media, 'progress');
// Fix keyboard focus issues
// https://github.com/sampotts/plyr/issues/317
frame.setAttribute('tabindex', -1);
}
});
// Check all loaded
if (parseInt(data.percent, 10) === 1) {
triggerEvent.call(player, player.media, 'canplaythrough');
}
player.embed.on('bufferstart', () => {
triggerEvent.call(player, player.media, 'waiting');
});
// Get duration as if we do it before load, it gives an incorrect value
// https://github.com/sampotts/plyr/issues/891
player.embed.getDuration().then(value => {
if (value !== player.media.duration) {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('bufferend', () => {
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('seeked', () => {
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
});
player.embed.on('play', () => {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('ended', () => {
player.media.paused = true;
triggerEvent.call(player, player.media, 'ended');
});
player.embed.on('pause', () => {
assurePlaybackState.call(player, false);
});
player.embed.on('error', detail => {
player.media.error = detail;
triggerEvent.call(player, player.media, 'error');
});
player.embed.on('timeupdate', (data) => {
player.media.seeking = false;
currentTime = data.seconds;
triggerEvent.call(player, player.media, 'timeupdate');
});
// Rebuild UI
setTimeout(() => ui.build.call(player), 0);
},
player.embed.on('progress', (data) => {
player.media.buffered = data.percent;
triggerEvent.call(player, player.media, 'progress');
// Check all loaded
if (parseInt(data.percent, 10) === 1) {
triggerEvent.call(player, player.media, 'canplaythrough');
}
// Get duration as if we do it before load, it gives an incorrect value
// https://github.com/sampotts/plyr/issues/891
player.embed.getDuration().then((value) => {
if (value !== player.media.duration) {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('seeked', () => {
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
});
player.embed.on('ended', () => {
player.media.paused = true;
triggerEvent.call(player, player.media, 'ended');
});
player.embed.on('error', (detail) => {
player.media.error = detail;
triggerEvent.call(player, player.media, 'error');
});
// Rebuild UI
if (config.customControls) {
setTimeout(() => ui.build.call(player), 0);
}
},
};
export default vimeo;
+401 -388
View File
@@ -7,428 +7,441 @@ import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
import loadImage from '../utils/loadImage';
import loadScript from '../utils/loadScript';
import loadImage from '../utils/load-image';
import loadScript from '../utils/load-script';
import { extend } from '../utils/objects';
import { format, generateId } from '../utils/strings';
import { setAspectRatio } from '../utils/style';
import { roundAspectRatio, setAspectRatio } from '../utils/style';
// Parse YouTube ID from URL
function parseId(url) {
if (is.empty(url)) {
return null;
}
if (is.empty(url)) {
return null;
}
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
return url.match(regex) ? RegExp.$2 : url;
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
return url.match(regex) ? RegExp.$2 : url;
}
// Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) {
this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true;
}
if (this.media.paused === play) {
this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
function getHost(config) {
if (config.noCookie) {
return 'https://www.youtube-nocookie.com';
}
if (window.location.protocol === 'http:') {
return 'http://www.youtube.com';
}
// Use YouTube's default
return undefined;
}
const youtube = {
setup() {
// Add embed class for responsive
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
setup() {
// Add embed class for responsive
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Setup API
if (is.object(window.YT) && is.function(window.YT.Player)) {
youtube.ready.call(this);
} else {
// Reference current global callback
const callback = window.onYouTubeIframeAPIReady;
// Set callback to process queue
window.onYouTubeIframeAPIReady = () => {
// Call global callback if set
if (is.function(callback)) {
callback();
}
youtube.ready.call(this);
};
// Load the SDK
loadScript(this.config.urls.youtube.sdk).catch((error) => {
this.debug.warn('YouTube API failed to load', error);
});
}
},
// Get the media title
getTitle(videoId) {
const url = format(this.config.urls.youtube.api, videoId);
fetch(url)
.then((data) => {
if (is.object(data)) {
const { title, height, width } = data;
// Set title
this.config.title = title;
ui.setTitle.call(this);
// Set aspect ratio
this.embed.ratio = roundAspectRatio(width, height);
}
setAspectRatio.call(this);
})
.catch(() => {
// Set aspect ratio
setAspectRatio.call(this);
});
},
// Setup API
if (is.object(window.YT) && is.function(window.YT.Player)) {
youtube.ready.call(this);
} else {
// Load the API
loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
// API ready
ready() {
const player = this;
const config = player.config.youtube;
// Ignore already setup (race condition)
const currentId = player.media && player.media.getAttribute('id');
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return;
}
// Setup callback for the API
// YouTube has it's own system of course...
window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || [];
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Add to queue
window.onYouTubeReadyCallbacks.push(() => {
youtube.ready.call(this);
});
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}
// Set callback to process queue
window.onYouTubeIframeAPIReady = () => {
window.onYouTubeReadyCallbacks.forEach(callback => {
callback();
});
};
}
},
// Replace the <iframe> with a <div> due to YouTube API issues
const videoId = parseId(source);
const id = generateId(player.provider);
// Replace media element
const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });
player.media = replaceElement(container, player.media);
// Get the media title
getTitle(videoId) {
// Try via undocumented API method first
// This method disappears now and then though...
// https://github.com/sampotts/plyr/issues/709
if (is.function(this.embed.getVideoData)) {
const { title } = this.embed.getVideoData();
// Only load the poster when using custom controls
if (config.customControls) {
const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
if (is.empty(title)) {
this.config.title = title;
ui.setTitle.call(this);
return;
}
}
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
.then((image) => ui.setPoster.call(player, image.src))
.then((src) => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!src.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
}
})
.catch(() => {});
}
// Or via Google API
const key = this.config.keys.google;
if (is.string(key) && !is.empty(key)) {
const url = format(this.config.urls.youtube.api, videoId, key);
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
player.embed = new window.YT.Player(player.media, {
videoId,
host: getHost(config),
playerVars: extend(
{},
{
// Autoplay
autoplay: player.config.autoplay ? 1 : 0,
// iframe interface language
hl: player.config.hl,
// Only show controls if not fully supported or opted out
controls: player.supported.ui && config.customControls ? 0 : 1,
// Disable keyboard as we handle it
disablekb: 1,
// Allow iOS inline playback
playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,
// Captions are flaky on YouTube
cc_load_policy: player.captions.active ? 1 : 0,
cc_lang_pref: player.config.captions.language,
// Tracking for stats
widget_referrer: window ? window.location.href : null,
},
config,
),
events: {
onError(event) {
// YouTube may fire onError twice, so only handle it once
if (!player.media.error) {
const code = event.data;
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
const message =
{
2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
101: 'The owner of the requested video does not allow it to be played in embedded players.',
150: 'The owner of the requested video does not allow it to be played in embedded players.',
}[code] || 'An unknown error occurred';
fetch(url)
.then(result => {
if (is.object(result)) {
this.config.title = result.items[0].snippet.title;
ui.setTitle.call(this);
}
})
.catch(() => {});
}
},
player.media.error = { code, message };
// API ready
ready() {
const player = this;
triggerEvent.call(player, player.media, 'error');
}
},
onPlaybackRateChange(event) {
// Get the instance
const instance = event.target;
// Ignore already setup (race condition)
const currentId = player.media.getAttribute('id');
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
triggerEvent.call(player, player.media, 'ratechange');
},
onReady(event) {
// Bail if onReady has already been called. See issue #1108
if (is.function(player.media.play)) {
return;
}
}
// Get the instance
const instance = event.target;
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get the title
youtube.getTitle.call(player, videoId);
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}
// Create a faux HTML5 API using the YouTube API
player.media.play = () => {
assurePlaybackState.call(player, true);
instance.playVideo();
};
// Replace the <iframe> with a <div> due to YouTube API issues
const videoId = parseId(source);
const id = generateId(player.provider);
player.media.pause = () => {
assurePlaybackState.call(player, false);
instance.pauseVideo();
};
// Get poster, if already set
const { poster } = player;
player.media.stop = () => {
instance.stopVideo();
};
// Replace media element
const container = createElement('div', { id, poster });
player.media = replaceElement(container, player.media);
player.media.duration = instance.getDuration();
player.media.paused = true;
// Id to poster wrapper
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
// 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(() => {});
const config = player.config.youtube;
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
player.embed = new window.YT.Player(id, {
videoId,
host: config.noCookie ? 'https://www.youtube-nocookie.com' : undefined,
playerVars: extend(
{},
{
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
disablekb: 1, // Disable keyboard as we handle it
playsinline: !player.config.fullscreen.iosNative ? 1 : 0, // Allow iOS inline playback
// Captions are flaky on YouTube
cc_load_policy: player.captions.active ? 1 : 0,
cc_lang_pref: player.config.captions.language,
// Tracking for stats
widget_referrer: window ? window.location.href : null,
},
config,
),
events: {
onError(event) {
// YouTube may fire onError twice, so only handle it once
if (!player.media.error) {
const code = event.data;
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
const message =
{
2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
101: 'The owner of the requested video does not allow it to be played in embedded players.',
150: 'The owner of the requested video does not allow it to be played in embedded players.',
}[code] || 'An unknown error occured';
player.media.error = { code, message };
triggerEvent.call(player, player.media, 'error');
}
},
onPlaybackRateChange(event) {
// Get the instance
const instance = event.target;
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
triggerEvent.call(player, player.media, 'ratechange');
},
onReady(event) {
// Bail if onReady has already been called. See issue #1108
if (is.function(player.media.play)) {
return;
}
// Get the instance
const instance = event.target;
// Get the title
youtube.getTitle.call(player, videoId);
// Create a faux HTML5 API using the YouTube API
player.media.play = () => {
assurePlaybackState.call(player, true);
instance.playVideo();
};
player.media.pause = () => {
assurePlaybackState.call(player, false);
instance.pauseVideo();
};
player.media.stop = () => {
instance.stopVideo();
};
player.media.duration = instance.getDuration();
player.media.paused = true;
// Seeking
player.media.currentTime = 0;
Object.defineProperty(player.media, 'currentTime', {
get() {
return Number(instance.getCurrentTime());
},
set(time) {
// If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
if (player.paused && !player.embed.hasPlayed) {
player.embed.mute();
}
// Set seeking state and trigger event
player.media.seeking = true;
triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
},
});
// Playback speed
Object.defineProperty(player.media, 'playbackRate', {
get() {
return instance.getPlaybackRate();
},
set(input) {
instance.setPlaybackRate(input);
},
});
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
get() {
return volume;
},
set(input) {
volume = input;
instance.setVolume(volume * 100);
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Muted
let { muted } = player.config;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Source
Object.defineProperty(player.media, 'currentSrc', {
get() {
return instance.getVideoUrl();
},
});
// Ended
Object.defineProperty(player.media, 'ended', {
get() {
return player.currentTime === player.duration;
},
});
// Get available speeds
player.options.speed = instance.getAvailablePlaybackRates();
// Set the tabindex to avoid focus entering iframe
if (player.supported.ui) {
player.media.setAttribute('tabindex', -1);
}
triggerEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'durationchange');
// Reset timer
clearInterval(player.timers.buffering);
// Setup buffering
player.timers.buffering = setInterval(() => {
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
// Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
triggerEvent.call(player, player.media, 'progress');
}
// Set last buffer point
player.media.lastBuffered = player.media.buffered;
// Bail if we're at 100%
if (player.media.buffered === 1) {
clearInterval(player.timers.buffering);
// Trigger event
triggerEvent.call(player, player.media, 'canplaythrough');
}
}, 200);
// Rebuild UI
setTimeout(() => ui.build.call(player), 50);
},
onStateChange(event) {
// Get the instance
const instance = event.target;
// Reset timer
clearInterval(player.timers.playing);
const seeked = player.media.seeking && [1, 2].includes(event.data);
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
}
// Handle events
// -1 Unstarted
// 0 Ended
// 1 Playing
// 2 Paused
// 3 Buffering
// 5 Video cued
switch (event.data) {
case -1:
// Update scrubber
triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
triggerEvent.call(player, player.media, 'progress');
break;
case 0:
assurePlaybackState.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) {
// YouTube needs a call to `stopVideo` before playing again
instance.stopVideo();
instance.playVideo();
} else {
triggerEvent.call(player, player.media, 'ended');
}
break;
case 1:
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (player.media.paused && !player.embed.hasPlayed) {
player.media.pause();
} else {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
triggerEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
// https://github.com/sampotts/plyr/issues/374
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
triggerEvent.call(player, player.media, 'durationchange');
}
}
break;
case 2:
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.muted) {
player.embed.unMute();
}
assurePlaybackState.call(player, false);
break;
default:
break;
}
triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},
// Seeking
player.media.currentTime = 0;
Object.defineProperty(player.media, 'currentTime', {
get() {
return Number(instance.getCurrentTime());
},
});
},
set(time) {
// If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
if (player.paused && !player.embed.hasPlayed) {
player.embed.mute();
}
// Set seeking state and trigger event
player.media.seeking = true;
triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
},
});
// Playback speed
Object.defineProperty(player.media, 'playbackRate', {
get() {
return instance.getPlaybackRate();
},
set(input) {
instance.setPlaybackRate(input);
},
});
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
get() {
return volume;
},
set(input) {
volume = input;
instance.setVolume(volume * 100);
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Muted
let { muted } = player.config;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
instance.setVolume(volume * 100);
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Source
Object.defineProperty(player.media, 'currentSrc', {
get() {
return instance.getVideoUrl();
},
});
// Ended
Object.defineProperty(player.media, 'ended', {
get() {
return player.currentTime === player.duration;
},
});
// Get available speeds
const speeds = instance.getAvailablePlaybackRates();
// Filter based on config
player.options.speed = speeds.filter((s) => player.config.speed.options.includes(s));
// Set the tabindex to avoid focus entering iframe
if (player.supported.ui && config.customControls) {
player.media.setAttribute('tabindex', -1);
}
triggerEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'durationchange');
// Reset timer
clearInterval(player.timers.buffering);
// Setup buffering
player.timers.buffering = setInterval(() => {
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
// Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
triggerEvent.call(player, player.media, 'progress');
}
// Set last buffer point
player.media.lastBuffered = player.media.buffered;
// Bail if we're at 100%
if (player.media.buffered === 1) {
clearInterval(player.timers.buffering);
// Trigger event
triggerEvent.call(player, player.media, 'canplaythrough');
}
}, 200);
// Rebuild UI
if (config.customControls) {
setTimeout(() => ui.build.call(player), 50);
}
},
onStateChange(event) {
// Get the instance
const instance = event.target;
// Reset timer
clearInterval(player.timers.playing);
const seeked = player.media.seeking && [1, 2].includes(event.data);
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
}
// Handle events
// -1 Unstarted
// 0 Ended
// 1 Playing
// 2 Paused
// 3 Buffering
// 5 Video cued
switch (event.data) {
case -1:
// Update scrubber
triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
triggerEvent.call(player, player.media, 'progress');
break;
case 0:
assurePlaybackState.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) {
// YouTube needs a call to `stopVideo` before playing again
instance.stopVideo();
instance.playVideo();
} else {
triggerEvent.call(player, player.media, 'ended');
}
break;
case 1:
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
player.media.pause();
} else {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
triggerEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
// https://github.com/sampotts/plyr/issues/374
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
triggerEvent.call(player, player.media, 'durationchange');
}
}
break;
case 2:
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.muted) {
player.embed.unMute();
}
assurePlaybackState.call(player, false);
break;
case 3:
// Trigger waiting event to add loading classes to container as the video buffers.
triggerEvent.call(player, player.media, 'waiting');
break;
default:
break;
}
triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},
},
});
},
};
export default youtube;
+726
View File
@@ -0,0 +1,726 @@
// Type definitions for plyr 3.5
// Project: https://plyr.io
// Definitions by: ondratra <https://github.com/ondratra>
// TypeScript Version: 3.0
export = Plyr;
export as namespace Plyr;
declare class Plyr {
/**
* Setup a new instance
*/
static setup(targets: NodeList | HTMLElement | HTMLElement[] | string, options?: Plyr.Options): Plyr[];
/**
* Check for support
* @param mediaType
* @param provider
* @param playsInline Whether the player has the playsinline attribute (only applicable to iOS 10+)
*/
static supported(mediaType?: Plyr.MediaType, provider?: Plyr.Provider, playsInline?: boolean): Plyr.Support;
constructor(targets: NodeList | HTMLElement | HTMLElement[] | string, options?: Plyr.Options);
/**
* Indicates if the current player is HTML5.
*/
readonly isHTML5: boolean;
/**
* Indicates if the current player is an embedded player.
*/
readonly isEmbed: boolean;
/**
* Indicates if the current player is playing.
*/
readonly playing: boolean;
/**
* Indicates if the current player is paused.
*/
readonly paused: boolean;
/**
* Indicates if the current player is stopped.
*/
readonly stopped: boolean;
/**
* Indicates if the current player has finished playback.
*/
readonly ended: boolean;
/**
* Returns a float between 0 and 1 indicating how much of the media is buffered
*/
readonly buffered: number;
/**
* Gets or sets the currentTime for the player. The setter accepts a float in seconds.
*/
currentTime: number;
/**
* Indicates if the current player is seeking.
*/
readonly seeking: boolean;
/**
* Returns the duration for the current media.
*/
readonly duration: number;
/**
* Gets or sets the volume for the player. The setter accepts a float between 0 and 1.
*/
volume: number;
/**
* Gets or sets the muted state of the player. The setter accepts a boolean.
*/
muted: boolean;
/**
* Indicates if the current media has an audio track.
*/
readonly hasAudio: boolean;
/**
* Gets or sets the speed for the player. The setter accepts a value in the options specified in your config. Generally the minimum should be 0.5.
*/
speed: number;
/**
* Gets or sets the quality for the player. The setter accepts a value from the options specified in your config.
*/
quality: number;
/**
* Gets or sets the current loop state of the player.
*/
loop: boolean;
/**
* Gets or sets the current source for the player.
*/
source: Plyr.SourceInfo;
/**
* Gets or sets the current poster image URL for the player.
*/
poster: string;
/**
* Gets or sets the autoplay state of the player.
*/
autoplay: boolean;
/**
* Gets or sets the caption track by index. 1 means the track is missing or captions is not active
*/
currentTrack: number;
/**
* Gets or sets the preferred captions language for the player. The setter accepts an ISO twoletter language code. Support for the languages is dependent on the captions you include.
* If your captions don't have any language data, or if you have multiple tracks with the same language, you may want to use currentTrack instead.
*/
language: string;
/**
* Gets or sets the picture-in-picture state of the player. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+.
*/
pip: boolean;
/**
* Gets or sets the aspect ratio for embedded players.
*/
ratio?: string;
/**
* Access Elements cache
*/
elements: Plyr.Elements;
/**
* Returns the current video Provider
*/
readonly provider: Plyr.Provider;
/**
* Returns the native API for Vimeo or Youtube players
*/
readonly embed?: any;
readonly fullscreen: Plyr.FullscreenControl;
/**
* Start playback.
* For HTML5 players, play() will return a Promise in some browsers - WebKit and Mozilla according to MDN at time of writing.
*/
play(): Promise<void> | void;
/**
* Pause playback.
*/
pause(): void;
/**
* Toggle playback, if no parameters are passed, it will toggle based on current status.
*/
togglePlay(toggle?: boolean): boolean;
/**
* Stop playback and reset to start.
*/
stop(): void;
/**
* Restart playback.
*/
restart(): void;
/**
* Rewind playback by the specified seek time. If no parameter is passed, the default seek time will be used.
*/
rewind(seekTime?: number): void;
/**
* Fast forward by the specified seek time. If no parameter is passed, the default seek time will be used.
*/
forward(seekTime?: number): void;
/**
* Increase volume by the specified step. If no parameter is passed, the default step will be used.
*/
increaseVolume(step?: number): void;
/**
* Increase volume by the specified step. If no parameter is passed, the default step will be used.
*/
decreaseVolume(step?: number): void;
/**
* Toggle captions display. If no parameter is passed, it will toggle based on current status.
*/
toggleCaptions(toggle?: boolean): void;
/**
* Trigger the airplay dialog on supported devices.
*/
airplay(): void;
/**
* Sets the preview thumbnails for the current source.
*/
setPreviewThumbnails(source: Plyr.PreviewThumbnailsOptions): void;
/**
* Toggle the controls (video only). Takes optional truthy value to force it on/off.
*/
toggleControls(toggle: boolean): void;
/**
* Add an event listener for the specified event.
*/
on<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
/**
* Add an event listener for the specified event once.
*/
once<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
/**
* Remove an event listener for the specified event.
*/
off<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
/**
* Check support for a mime type.
*/
supports(type: string): boolean;
/**
* Destroy lib instance
* @param {Function} callback - Callback for when destroy is complete
* @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)
*/
destroy(callback?: (...args: any[]) => void, soft?: boolean): void;
}
declare namespace Plyr {
type MediaType = 'audio' | 'video';
type Provider = 'html5' | 'youtube' | 'vimeo';
type StandardEventMap = {
progress: PlyrEvent;
playing: PlyrEvent;
play: PlyrEvent;
pause: PlyrEvent;
timeupdate: PlyrEvent;
volumechange: PlyrEvent;
seeking: PlyrEvent;
seeked: PlyrEvent;
ratechange: PlyrEvent;
ended: PlyrEvent;
enterfullscreen: PlyrEvent;
exitfullscreen: PlyrEvent;
captionsenabled: PlyrEvent;
captionsdisabled: PlyrEvent;
languagechange: PlyrEvent;
controlshidden: PlyrEvent;
controlsshown: PlyrEvent;
ready: PlyrEvent;
};
// For retrocompatibility, we keep StandardEvent
type StandardEvent = keyof Plyr.StandardEventMap;
type Html5EventMap = {
loadstart: PlyrEvent;
loadeddata: PlyrEvent;
loadedmetadata: PlyrEvent;
canplay: PlyrEvent;
canplaythrough: PlyrEvent;
stalled: PlyrEvent;
waiting: PlyrEvent;
emptied: PlyrEvent;
cuechange: PlyrEvent;
error: PlyrEvent;
};
// For retrocompatibility, we keep Html5Event
type Html5Event = keyof Plyr.Html5EventMap;
type YoutubeEventMap = {
statechange: PlyrStateChangeEvent;
qualitychange: PlyrEvent;
qualityrequested: PlyrEvent;
};
// For retrocompatibility, we keep YoutubeEvent
type YoutubeEvent = keyof Plyr.YoutubeEventMap;
type PlyrEventMap = StandardEventMap & Html5EventMap & YoutubeEventMap;
interface FullscreenControl {
/**
* Indicates if the current player is in fullscreen mode.
*/
readonly active: boolean;
/**
* Indicates if the current player has fullscreen enabled.
*/
readonly enabled: boolean;
/**
* Enter fullscreen. If fullscreen is not supported, a fallback ""full window/viewport"" is used instead.
*/
enter(): void;
/**
* Exit fullscreen.
*/
exit(): void;
/**
* Toggle fullscreen.
*/
toggle(): void;
}
interface Options {
/**
* Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below.
*/
enabled?: boolean;
/**
* Display debugging information in the console
*/
debug?: boolean;
/**
* If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function;
* id (the unique id for the player), seektime (the seektime step in seconds), and title (the media title). See CONTROLS.md for more info on how the html needs to be structured.
* Defaults to ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']
*/
controls?: string | string[] | ((id: string, seektime: number, title: string) => unknown) | Element;
/**
* If you're using the default controls are used then you can specify which settings to show in the menu
* Defaults to ['captions', 'quality', 'speed', 'loop']
*/
settings?: string[];
/**
* Used for internationalization (i18n) of the text within the UI.
*/
i18n?: any;
/**
* Load the SVG sprite specified as the iconUrl option (if a URL). If false, it is assumed you are handling sprite loading yourself.
*/
loadSprite?: boolean;
/**
* Specify a URL or path to the SVG sprite. See the SVG section for more info.
*/
iconUrl?: string;
/**
* Specify the id prefix for the icons used in the default controls (e.g. plyr-play would be plyr).
* This is to prevent clashes if you're using your own SVG sprite but with the default controls.
* Most people can ignore this option.
*/
iconPrefix?: string;
/**
* Specify a URL or path to a blank video file used to properly cancel network requests.
*/
blankVideo?: string;
/**
* Autoplay the media on load. This is generally advised against on UX grounds. It is also disabled by default in some browsers.
* If the autoplay attribute is present on a <video> or <audio> element, this will be automatically set to true.
*/
autoplay?: boolean;
/**
* Only allow one player playing at once.
*/
autopause?: boolean;
/**
* The time, in seconds, to seek when a user hits fast forward or rewind.
*/
seekTime?: number;
/**
* A number, between 0 and 1, representing the initial volume of the player.
*/
volume?: number;
/**
* Whether to start playback muted. If the muted attribute is present on a <video> or <audio> element, this will be automatically set to true.
*/
muted?: boolean;
/**
* Click (or tap) of the video container will toggle play/pause.
*/
clickToPlay?: boolean;
/**
* Disable right click menu on video to help as very primitive obfuscation to prevent downloads of content.
*/
disableContextMenu?: boolean;
/**
* Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen.
* As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly.
*/
hideControls?: boolean;
/**
* Reset the playback to the start once playback is complete.
*/
resetOnEnd?: boolean;
/**
* Enable keyboard shortcuts for focused players only or globally
*/
keyboard?: KeyboardOptions;
/**
* controls: Display control labels as tooltips on :hover & :focus (by default, the labels are screen reader only).
* seek: Display a seek tooltip to indicate on click where the media would seek to.
*/
tooltips?: TooltipOptions;
/**
* Specify a custom duration for media.
*/
duration?: number;
/**
* Displays the duration of the media on the metadataloaded event (on startup) in the current time display.
* This will only work if the preload attribute is not set to none (or is not set at all) and you choose not to display the duration (see controls option).
*/
displayDuration?: boolean;
/**
* Display the current time as a countdown rather than an incremental counter.
*/
invertTime?: boolean;
/**
* Allow users to click to toggle the above.
*/
toggleInvert?: boolean;
/**
* Allows binding of event listeners to the controls before the default handlers. See the defaults.js for available listeners.
* If your handler prevents default on the event (event.preventDefault()), the default handler will not fire.
*/
listeners?: { [key: string]: (error: PlyrEvent) => void };
/**
* active: Toggles if captions should be active by default. language: Sets the default language to load (if available). 'auto' uses the browser language.
* update: Listen to changes to tracks and update menu. This is needed for some streaming libraries, but can result in unselectable language options).
*/
captions?: CaptionOptions;
/**
* enabled: Toggles whether fullscreen should be enabled. fallback: Allow fallback to a full-window solution.
* iosNative: whether to use native iOS fullscreen when entering fullscreen (no custom controls)
*/
fullscreen?: FullScreenOptions;
/**
* The aspect ratio you want to use for embedded players.
*/
ratio?: string;
/**
* enabled: Allow use of local storage to store user settings. key: The key name to use.
*/
storage?: StorageOptions;
/**
* selected: The default speed for playback. options: The speed options to display in the UI. YouTube and Vimeo will ignore any options outside of the 0.5-2 range, so options outside of this range will be hidden automatically.
*/
speed?: SpeedOptions;
/**
* Currently only supported by YouTube. default is the default quality level, determined by YouTube. options are the options to display.
*/
quality?: QualityOptions;
/**
* active: Whether to loop the current video. If the loop attribute is present on a <video> or <audio> element,
* this will be automatically set to true This is an object to support future functionality.
*/
loop?: LoopOptions;
/**
* enabled: Whether to enable vi.ai ads. publisherId: Your unique vi.ai publisher ID.
*/
ads?: AdOptions;
/**
* Vimeo Player Options.
*/
vimeo?: object;
/**
* Youtube Player Options.
*/
youtube?: object;
/**
* Preview Thumbnails Options.
*/
previewThumbnails?: PreviewThumbnailsOptions;
/**
* Media Metadata Options.
*/
mediaMetadata?: MediaMetadataOptions;
/**
* Markers Options
*/
markers?: MarkersOptions;
}
interface QualityOptions {
default: number;
forced?: boolean;
onChange?: (quality: number) => void;
options: number[];
}
interface LoopOptions {
active: boolean;
}
interface AdOptions {
enabled: boolean;
publisherId?: string;
tagUrl?: string;
}
interface SpeedOptions {
selected: number;
options: number[];
}
interface KeyboardOptions {
focused?: boolean;
global?: boolean;
}
interface TooltipOptions {
controls?: boolean;
seek?: boolean;
}
interface FullScreenOptions {
enabled?: boolean;
fallback?: boolean | 'force';
allowAudio?: boolean;
iosNative?: boolean;
container?: string;
}
interface CaptionOptions {
active?: boolean;
language?: string;
update?: boolean;
}
interface StorageOptions {
enabled?: boolean;
key?: string;
}
interface PreviewThumbnailsOptions {
enabled?: boolean;
src?: string | string[];
}
interface MediaMetadataArtwork {
src: string;
sizes?: string;
type: string;
}
interface MediaMetadataOptions {
title?: string;
artist?: string;
album?: string;
artwork?: MediaMetadataArtwork[];
}
interface MarkersPoints {
time: number;
label: string;
}
interface MarkersOptions {
enabled: boolean;
points: MarkersPoints[];
}
export interface Elements {
buttons: {
airplay?: HTMLButtonElement;
captions?: HTMLButtonElement;
download?: HTMLButtonElement;
fastForward?: HTMLButtonElement;
fullscreen?: HTMLButtonElement;
mute?: HTMLButtonElement;
pip?: HTMLButtonElement;
play?: HTMLButtonElement | HTMLButtonElement[];
restart?: HTMLButtonElement;
rewind?: HTMLButtonElement;
settings?: HTMLButtonElement;
};
captions: HTMLElement | null;
container: HTMLElement | null;
controls: HTMLElement | null;
fullscreen: HTMLElement | null;
wrapper: HTMLElement | null;
}
interface SourceInfo {
/**
* Note: YouTube and Vimeo are currently not supported as audio sources.
*/
type: MediaType;
/**
* Title of the new media. Used for the aria-label attribute on the play button, and outer container. YouTube and Vimeo are populated automatically.
*/
title?: string;
/**
* This is an array of sources. For HTML5 media, the properties of this object are mapped directly to HTML attributes so more can be added to the object if required.
*/
sources: Source[];
/**
* The URL for the poster image (HTML5 video only).
*/
poster?: string;
/**
* An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above,
* it will render as <track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default> and similar for the French version.
* Booleans are converted to HTML5 value-less attributes.
*/
tracks?: Track[];
/**
* Enable or disable preview thumbnails for current source
*/
previewThumbnails?: Plyr.PreviewThumbnailsOptions;
}
interface Source {
/**
* The URL of the media file (or YouTube/Vimeo URL).
*/
src: string;
/**
* The MIME type of the media file (if HTML5).
*/
type?: string;
provider?: Provider;
size?: number;
}
type TrackKind = 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
interface Track {
/**
* Indicates how the text track is meant to be used
*/
kind: TrackKind;
/**
* Indicates a user-readable title for the track
*/
label: string;
/**
* The language of the track text data. It must be a valid BCP 47 language tag. If the kind attribute is set to subtitles, then srclang must be defined.
*/
srcLang?: string;
/**
* The URL of the track (.vtt file).
*/
src: string;
default?: boolean;
}
interface PlyrEvent extends CustomEvent {
readonly detail: { readonly plyr: Plyr };
}
enum YoutubeState {
UNSTARTED = -1,
ENDED = 0,
PLAYING = 1,
PAUSED = 2,
BUFFERING = 3,
CUED = 5,
}
interface PlyrStateChangeEvent extends CustomEvent {
readonly detail: {
readonly plyr: Plyr;
readonly code: YoutubeState;
};
}
interface Support {
api: boolean;
ui: boolean;
}
}

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