├── site
├── src
│ ├── js
│ │ ├── compare.js
│ │ ├── index.js
│ │ ├── utils
│ │ │ ├── dom.js
│ │ │ ├── url.js
│ │ │ └── utils.js
│ │ ├── routing.js
│ │ ├── helpers
│ │ │ └── dropdown.js
│ │ ├── matrix.js
│ │ ├── site.js
│ │ ├── rendition-observer.js
│ │ ├── logger.js
│ │ ├── demo.js
│ │ └── ga.js
│ ├── css
│ │ ├── matrix.css
│ │ ├── compare.css
│ │ ├── main.css
│ │ ├── base.css
│ │ ├── home.css
│ │ ├── player.css
│ │ ├── demo.css
│ │ ├── prism-ghcolors.css
│ │ ├── prism-dracula.css
│ │ └── inter.css
│ ├── _data
│ │ ├── dev
│ │ │ ├── muxify.js
│ │ │ ├── lighthouse.js
│ │ │ ├── playerconfig.js
│ │ │ ├── compare.js
│ │ │ ├── matrix.js
│ │ │ ├── players.js
│ │ │ └── site.yaml
│ │ ├── prod
│ │ │ ├── muxify.js
│ │ │ ├── lighthouse.js
│ │ │ ├── playerconfig.js
│ │ │ ├── compare.js
│ │ │ ├── matrix.js
│ │ │ ├── players.js
│ │ │ └── site.yaml
│ │ ├── matrix.yaml
│ │ ├── playerconfig.js
│ │ ├── compare.yaml
│ │ ├── lighthouse.js
│ │ ├── players.yaml
│ │ └── muxify.js
│ ├── favicon.ico
│ ├── images
│ │ ├── bg-grid.png
│ │ ├── mux-logo.png
│ │ ├── symbol-defs.svg
│ │ └── playerx-logo.svg
│ ├── fonts
│ │ ├── Inter-Black.woff
│ │ ├── Inter-Black.woff2
│ │ ├── Inter-Bold.woff
│ │ ├── Inter-Bold.woff2
│ │ ├── Inter-Italic.woff
│ │ ├── Inter-Light.woff
│ │ ├── Inter-Light.woff2
│ │ ├── Inter-Medium.woff
│ │ ├── Inter-Thin.woff
│ │ ├── Inter-Thin.woff2
│ │ ├── Inter.var.woff2
│ │ ├── Inter-Italic.woff2
│ │ ├── Inter-Medium.woff2
│ │ ├── Inter-Regular.woff
│ │ ├── Inter-Regular.woff2
│ │ ├── Inter-SemiBold.woff
│ │ ├── Inter-BlackItalic.woff
│ │ ├── Inter-BoldItalic.woff
│ │ ├── Inter-BoldItalic.woff2
│ │ ├── Inter-ExtraBold.woff
│ │ ├── Inter-ExtraBold.woff2
│ │ ├── Inter-ExtraLight.woff
│ │ ├── Inter-ExtraLight.woff2
│ │ ├── Inter-LightItalic.woff
│ │ ├── Inter-SemiBold.woff2
│ │ ├── Inter-ThinItalic.woff
│ │ ├── Inter-ThinItalic.woff2
│ │ ├── Inter-italic.var.woff2
│ │ ├── Inter-roman.var.woff2
│ │ ├── Inter-BlackItalic.woff2
│ │ ├── Inter-LightItalic.woff2
│ │ ├── Inter-MediumItalic.woff
│ │ ├── Inter-MediumItalic.woff2
│ │ ├── Inter-ExtraBoldItalic.woff
│ │ ├── Inter-ExtraBoldItalic.woff2
│ │ ├── Inter-ExtraLightItalic.woff
│ │ ├── Inter-SemiBoldItalic.woff
│ │ ├── Inter-SemiBoldItalic.woff2
│ │ ├── Inter-ExtraLightItalic.woff2
│ │ └── inter.css
│ ├── pages
│ │ ├── add-ons
│ │ │ └── index.njk
│ │ ├── docs
│ │ │ ├── elements.md
│ │ │ ├── index.md
│ │ │ └── player.md
│ │ ├── license.njk
│ │ ├── forum.njk
│ │ ├── index.njk
│ │ ├── compare
│ │ │ ├── index.njk
│ │ │ └── compare.njk
│ │ ├── demo.njk
│ │ ├── players.njk
│ │ ├── lighthouse.njk
│ │ ├── demo-player.njk
│ │ └── matrix.njk
│ ├── _includes
│ │ ├── layouts
│ │ │ ├── empty.njk
│ │ │ ├── demo.njk
│ │ │ ├── post.md
│ │ │ ├── docs.njk
│ │ │ ├── player.njk
│ │ │ ├── lighthouse.njk
│ │ │ ├── base.njk
│ │ │ ├── root.njk
│ │ │ └── demo-player.njk
│ │ ├── footer.njk
│ │ ├── sidebar.njk
│ │ └── header.njk
│ ├── utils
│ │ ├── minify-html.js
│ │ ├── save-seed.js
│ │ ├── filters
│ │ │ ├── date.js
│ │ │ ├── section.js
│ │ │ └── squash.js
│ │ └── pipes.js
│ └── sitemap.njk
├── babel.config.js
├── tailwind.config.js
├── rollup.config.mjs
├── package.json
└── .eleventy.js
├── tsconfig.json
├── src
├── config
│ ├── html.js
│ ├── streamable.js
│ ├── muxvideo.js
│ ├── vimeo.js
│ ├── apivideo.js
│ ├── dashjs.js
│ ├── shakaplayer.js
│ ├── cloudflare.js
│ ├── hlsjs.js
│ ├── theoplayer.js
│ ├── youtube.js
│ ├── videojs.js
│ ├── wistia.js
│ ├── index.js
│ ├── jwplayer.js
│ ├── brightcove.js
│ ├── vidyard.js
│ ├── dailymotion.js
│ ├── cloudinary.js
│ ├── facebook.js
│ └── helpers.js
├── options.js
├── playerx.js
└── playerx.d.ts
├── .github
└── workflows
│ ├── ci.yml
│ └── cloudflare.yml
├── .gitignore
├── package.json
└── README.md
/site/src/js/compare.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/site/src/css/matrix.css:
--------------------------------------------------------------------------------
1 | .grid-btn {
2 | @apply mx-1;
3 | }
4 |
--------------------------------------------------------------------------------
/site/src/js/index.js:
--------------------------------------------------------------------------------
1 | import './demo.js';
2 | import './routing.js';
3 |
--------------------------------------------------------------------------------
/site/src/_data/dev/muxify.js:
--------------------------------------------------------------------------------
1 | module.exports = require('../muxify.js');
2 |
--------------------------------------------------------------------------------
/site/src/_data/prod/muxify.js:
--------------------------------------------------------------------------------
1 | module.exports = require('../muxify.js');
2 |
--------------------------------------------------------------------------------
/site/src/_data/dev/lighthouse.js:
--------------------------------------------------------------------------------
1 | module.exports = require('../lighthouse.js');
2 |
--------------------------------------------------------------------------------
/site/src/_data/prod/lighthouse.js:
--------------------------------------------------------------------------------
1 | module.exports = require('../lighthouse.js');
2 |
--------------------------------------------------------------------------------
/site/src/_data/dev/playerconfig.js:
--------------------------------------------------------------------------------
1 | module.exports = require('../playerconfig.js');
2 |
--------------------------------------------------------------------------------
/site/src/_data/prod/playerconfig.js:
--------------------------------------------------------------------------------
1 | module.exports = require('../playerconfig.js');
2 |
--------------------------------------------------------------------------------
/site/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/favicon.ico
--------------------------------------------------------------------------------
/site/src/images/bg-grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/images/bg-grid.png
--------------------------------------------------------------------------------
/site/src/images/mux-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/images/mux-logo.png
--------------------------------------------------------------------------------
/site/src/fonts/Inter-Black.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-Black.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-Black.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-Black.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-Bold.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-Bold.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-Italic.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-Light.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-Light.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-Medium.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-Thin.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-Thin.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-Thin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-Thin.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter.var.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter.var.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-Italic.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-Medium.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-Regular.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-Regular.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-SemiBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-SemiBold.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-BlackItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-BlackItalic.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-BoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-BoldItalic.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-BoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-BoldItalic.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-ExtraBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-ExtraBold.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-ExtraBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-ExtraBold.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-ExtraLight.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-ExtraLight.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-ExtraLight.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-ExtraLight.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-LightItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-LightItalic.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-SemiBold.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-ThinItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-ThinItalic.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-ThinItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-ThinItalic.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-italic.var.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-italic.var.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-roman.var.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-roman.var.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-BlackItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-BlackItalic.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-LightItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-LightItalic.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-MediumItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-MediumItalic.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-MediumItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-MediumItalic.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-ExtraBoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-ExtraBoldItalic.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-ExtraBoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-ExtraBoldItalic.woff2
--------------------------------------------------------------------------------
/site/src/fonts/Inter-ExtraLightItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-ExtraLightItalic.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-SemiBoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-SemiBoldItalic.woff
--------------------------------------------------------------------------------
/site/src/fonts/Inter-SemiBoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-SemiBoldItalic.woff2
--------------------------------------------------------------------------------
/site/src/pages/add-ons/index.njk:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: false
3 | # eleventyNavigation:
4 | # key: Add-ons
5 | # order: 1
6 | ---
7 |
--------------------------------------------------------------------------------
/site/src/fonts/Inter-ExtraLightItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luwes/playerx/HEAD/site/src/fonts/Inter-ExtraLightItalic.woff2
--------------------------------------------------------------------------------
/site/src/pages/docs/elements.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: false
3 | eleventyNavigation:
4 | key: elements
5 | order: 2
6 | tags:
7 | - docs
8 | ---
9 |
--------------------------------------------------------------------------------
/site/src/pages/license.njk:
--------------------------------------------------------------------------------
1 | ---
2 | eleventyNavigation:
3 | key: License
4 | url: /#choose-license
5 | permalink: false
6 | tags:
7 |
8 | ---
9 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/empty.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/root.njk" %}
2 |
3 | {% block foot %}
4 |
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/site/src/pages/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: false
3 | eleventyNavigation:
4 | key: docs
5 | title: Docs
6 | url: /docs/playerx/
7 | order: 1
8 | ---
9 |
--------------------------------------------------------------------------------
/site/src/_data/dev/compare.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const yaml = require('js-yaml');
3 |
4 | module.exports = yaml.load(fs.readFileSync(`${__dirname}/../compare.yaml`, 'utf8'));
5 |
--------------------------------------------------------------------------------
/site/src/_data/dev/matrix.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const yaml = require('js-yaml');
3 |
4 | module.exports = yaml.load(fs.readFileSync(`${__dirname}/../matrix.yaml`, 'utf8'));
5 |
--------------------------------------------------------------------------------
/site/src/_data/dev/players.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const yaml = require('js-yaml');
3 |
4 | module.exports = yaml.load(fs.readFileSync(`${__dirname}/../players.yaml`, 'utf8'));
5 |
--------------------------------------------------------------------------------
/site/src/_data/prod/compare.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const yaml = require('js-yaml');
3 |
4 | module.exports = yaml.load(fs.readFileSync(`${__dirname}/../compare.yaml`, 'utf8'));
5 |
--------------------------------------------------------------------------------
/site/src/_data/prod/matrix.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const yaml = require('js-yaml');
3 |
4 | module.exports = yaml.load(fs.readFileSync(`${__dirname}/../matrix.yaml`, 'utf8'));
5 |
--------------------------------------------------------------------------------
/site/src/_data/prod/players.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const yaml = require('js-yaml');
3 |
4 | module.exports = yaml.load(fs.readFileSync(`${__dirname}/../players.yaml`, 'utf8'));
5 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/demo.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/base.njk" %}
2 |
3 | {% block head %}
4 | {{ super() }}
5 |
6 |
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/site/src/pages/forum.njk:
--------------------------------------------------------------------------------
1 | ---
2 | eleventyNavigation:
3 | key: Forum
4 | order: 4
5 | url: https://github.com/luwes/playerx/discussions
6 | permalink: false
7 | tags:
8 | - navEnd
9 | ---
10 |
--------------------------------------------------------------------------------
/site/src/_includes/footer.njk:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "noEmit": true,
7 | "strict": true,
8 | "allowJs": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/site/src/pages/index.njk:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /
3 | title: "Playerx for Developers"
4 | ---
5 |
6 | {% extends "layouts/root.njk" %}
7 |
8 | {% block head %}
9 |
10 | {{ super() }}
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/src/config/html.js:
--------------------------------------------------------------------------------
1 | export const type = 'inline'
2 | export const key = 'html'
3 | export const name = 'HTML'
4 | export const srcPattern = '\\.(mp[43]|og[gva]|web[ma]|mov|m4[va])($|\\?)'
5 | export const version = '5'
6 | export const html = '{{video}}'
7 | export const setup = '{{html}}'
8 |
--------------------------------------------------------------------------------
/site/src/_data/prod/site.yaml:
--------------------------------------------------------------------------------
1 |
2 | url: https://dev.playerx.io
3 | apiUrl: https://api.playerx.io
4 | oEmbedUrl: https://api.playerx.io
5 | githubUrl: https://github.com/luwes/playerx
6 | defaultPlayerSrc: https://vimeo.com/648359100
7 | defaultIframeSrc: https://player.vimeo.com/video/648359100
8 |
--------------------------------------------------------------------------------
/site/src/_data/dev/site.yaml:
--------------------------------------------------------------------------------
1 |
2 | url: http://dev.playerx.test
3 | apiUrl: http://api.playerx.test:1337
4 | oEmbedUrl: http://api.playerx.test:8787
5 | githubUrl: https://github.com/luwes/playerx
6 | defaultPlayerSrc: https://vimeo.com/648359100
7 | defaultIframeSrc: https://player.vimeo.com/video/648359100
8 |
--------------------------------------------------------------------------------
/site/src/_data/matrix.yaml:
--------------------------------------------------------------------------------
1 | players:
2 | muxplayer: true
3 | apivideo: true
4 | vimeo: true
5 | youtube: true
6 | dailymotion: true
7 | brightcove: true
8 | facebook: true
9 | streamable: true
10 | wistia: true
11 | jwplayer: true
12 | vidyard: true
13 | cloudinary: true
14 | cloudflare: true
15 |
--------------------------------------------------------------------------------
/site/src/pages/compare/index.njk:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /compare/
3 | title: "Playerx - Compare video platforms"
4 | ---
5 |
6 | {% extends "layouts/root.njk" %}
7 |
8 | {% block head %}
9 |
10 | {{ super() }}
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v3
11 | - uses: actions/setup-node@v3
12 | with:
13 | cache: 'npm'
14 | - run: npm ci
15 | - run: npm test
16 |
--------------------------------------------------------------------------------
/site/src/css/compare.css:
--------------------------------------------------------------------------------
1 |
2 | .compare-grid {
3 | grid-template-columns:
4 | minmax(300px, 1fr)
5 | repeat(var(--cols), minmax(155px, 1fr));
6 | }
7 |
8 | .grid-cell {
9 | @apply border-b px-5 py-4;
10 | }
11 |
12 | .category_performance_median {
13 | @apply py-0;
14 | }
15 |
16 | .platform-header {
17 | @apply px-4 py-3;
18 | }
19 |
--------------------------------------------------------------------------------
/site/src/utils/minify-html.js:
--------------------------------------------------------------------------------
1 | const htmlmin = require('html-minifier');
2 |
3 | module.exports = function(content, outputPath) {
4 | if (outputPath && outputPath.endsWith('.html')) {
5 | let minified = htmlmin.minify(content, {
6 | useShortDoctype: true,
7 | removeComments: true,
8 | collapseWhitespace: true
9 | });
10 | return minified;
11 | }
12 | return content;
13 | };
14 |
--------------------------------------------------------------------------------
/site/src/pages/demo.njk:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: demo/
3 | title: "Playerx - API Demo"
4 | menu: Demo
5 | eleventyNavigation:
6 | key: Demo
7 | order: 1
8 | tags:
9 | - main
10 | - demo
11 | ---
12 |
13 | {% extends "layouts/root.njk" %}
14 |
15 | {% block head %}
16 |
17 | {{ super() }}
18 | {% endblock %}
19 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/post.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layouts/base.njk
3 | pageClass: posts
4 | templateEngineOverride: njk, md
5 | ---
6 |
7 |
8 | Posted as an example, on
9 |
10 |
11 | {{ content | safe }}
12 |
17 |
18 |
--------------------------------------------------------------------------------
/site/src/css/main.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss/base";
2 | @import "tailwindcss/components";
3 | @import "tailwindcss/utilities";
4 |
5 | /*! purgecss start ignore */
6 | @import "inter.css";
7 | @import "base.css";
8 | @import "prism-dracula.css";
9 | @import "prism-ghcolors.css";
10 | @import "home.css";
11 | @import "demo.css";
12 | @import "player.css";
13 | @import "compare.css";
14 | @import "matrix.css";
15 | /*! purgecss end ignore */
16 |
--------------------------------------------------------------------------------
/site/src/utils/save-seed.js:
--------------------------------------------------------------------------------
1 | // Handy to save the results to a local file
2 | // to prime the dev data source
3 |
4 | const fs = require('fs');
5 |
6 | module.exports = (data, path) => {
7 | if (process.env.NODE_ENV == 'seed') {
8 | fs.writeFile(path, data, err => {
9 | if (err) {
10 | console.log(err);
11 | } else {
12 | console.log(`Data saved for dev: ${path}`);
13 | }
14 | });
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/site/src/sitemap.njk:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /sitemap.xml
3 | eleventyExcludeFromCollections: true
4 | ---
5 |
6 |
7 | {%- for page in collections.all %}
8 | {% set absoluteUrl %}{{ page.url | url | absoluteUrl(site.url) }}{% endset %}
9 |
10 | {{ absoluteUrl }}
11 | {{ page.date | htmlDateString }}
12 |
13 | {%- endfor %}
14 |
15 |
--------------------------------------------------------------------------------
/site/src/_data/playerconfig.js:
--------------------------------------------------------------------------------
1 | const yaml = require('js-yaml');
2 | const fs = require('fs');
3 |
4 | module.exports = async () => {
5 | let players;
6 | let input;
7 | try {
8 | input = fs.readFileSync(`${__dirname}/players.yaml`, 'utf8');
9 | players = yaml.load(input);
10 | } catch (e) {
11 | console.log(e);
12 | }
13 |
14 | return JSON.stringify(players.reduce((acc, { key, options }) => {
15 | if (options) acc[key] = options;
16 | return acc;
17 | }, {}));
18 | };
19 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/docs.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/base.njk" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 | {% include "sidebar.njk" %}
9 |
10 |
11 | {{ content | safe }}
12 |
13 |
14 |
15 |
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/site/src/utils/filters/date.js:
--------------------------------------------------------------------------------
1 | const { DateTime } = require('luxon');
2 |
3 | // Add a friendly date filter to nunjucks.
4 | // Defaults to format of LLLL d, y unless an
5 | // alternate is passed as a parameter.
6 | // {{ date | friendlyDate('OPTIONAL FORMAT STRING') }}
7 | // List of supported tokens: https://moment.github.io/luxon/docs/manual/formatting.html#table-of-tokens
8 |
9 | module.exports = (dateObj, format = 'LLL d, y') => {
10 | return DateTime.fromJSDate(dateObj, {
11 | zone: 'utc'
12 | }).toFormat(format);
13 | };
14 |
--------------------------------------------------------------------------------
/site/babel.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | presets: [
4 | [
5 | '@babel/preset-env',
6 | {
7 | modules: false,
8 | loose: false,
9 | targets: {
10 | browsers: ['ie >= 11']
11 | }
12 | }
13 | ]
14 | ],
15 | plugins: [
16 | ['sinuous/babel-plugin-htm', {
17 | import: 'sinuous/hydrate',
18 | pragma: 'd',
19 | tag: 'dhtml'
20 | }, 'for hydrate'],
21 | ['sinuous/babel-plugin-htm', {
22 | import: 'sinuous'
23 | }],
24 | ],
25 | };
26 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/player.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/root.njk" %}
2 |
3 | {% block head %}
4 |
5 |
6 | {% endblock %}
7 |
8 | {% block main %}
9 |
10 |
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/site/src/_includes/sidebar.njk:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | *.sublime-project
4 | yarn-error.log
5 | .nyc_output/
6 | coverage
7 | *.lcov
8 |
9 | dist/
10 | module/
11 | umd/
12 | esm/
13 | /site/public/
14 | /site/public/index.html
15 | /site/public/brightcove/
16 | /site/public/dailymotion/
17 | /site/public/facebook/
18 | /site/public/jw-player/
19 | /site/public/soundcloud/
20 | /site/public/streamable/
21 | /site/public/twitch/
22 | /site/public/vidyard/
23 | /site/public/vimeo/
24 | /site/public/wistia/
25 | /site/public/youtube/
26 | /bench/.env
27 |
28 | # Local Netlify folder
29 | .netlify
30 | .env
31 | *.sublime-*
32 |
--------------------------------------------------------------------------------
/src/config/streamable.js:
--------------------------------------------------------------------------------
1 | // https://github.com/embedly/player.js
2 | export const type = 'iframe'
3 | export const key = 'streamable'
4 | export const name = 'Streamable'
5 | export const url = 'https://streamable.com'
6 | export const srcPattern = 'streamable\\.com/(?:o/)?(\\w+)$'
7 | export const embedUrl = 'https://streamable.com/o/{{metaId}}?{{params}}'
8 | export const jsUrl = 'https://cdn.embed.ly/player-0.1.0.min.js'
9 | export const apiVar = 'playerjs'
10 | export const version = '1.x.x'
11 | export const html = '{{iframe}}';
12 | export const scriptText = `{{callback}}(new {{apiVar}}.Player({{node}}));`
13 |
--------------------------------------------------------------------------------
/src/config/muxvideo.js:
--------------------------------------------------------------------------------
1 | // https://github.com/muxinc/elements/tree/main/packages/mux-video
2 | export const type = 'inline'
3 | export const key = 'muxvideo'
4 | export const name = 'Mux'
5 | export const url = 'https://mux.com'
6 | export const srcPattern = '\\?player=muxvideo|stream\\.mux\\.com/(\\w+)\\.'
7 | export const pkg = '@mux-elements/mux-video'
8 | export const jsUrl = '{{npmCdn}}/{{pkg}}@{{version}}/dist/index.js'
9 | export const apiVar = 'MuxVideoElement'
10 | export const version = '0.15.0'
11 | export const html = ''
12 | export const scriptText = `{{callback}}({{node}});`
13 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/lighthouse.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/root.njk" %}
2 |
3 | {% block head %}
4 |
5 |
6 | {% endblock %}
7 |
8 | {% block main %}
9 |
10 |
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/src/config/vimeo.js:
--------------------------------------------------------------------------------
1 | // https://github.com/vimeo/player.js
2 | export const type = 'iframe'
3 | export const key = 'vimeo'
4 | export const name = 'Vimeo'
5 | export const url = 'https://vimeo.com'
6 | export const srcPattern = 'vimeo\\.com/(?:video/)?(\\d+)'
7 | export const embedUrl = 'https://player.vimeo.com/video/{{metaId}}?{{params}}'
8 | export const pkg = '@vimeo/player'
9 | export const jsUrl = '{{npmCdn}}/{{pkg}}@{{version}}/dist/player.min.js'
10 | export const apiVar = 'Vimeo'
11 | export const version = '2.16.2'
12 | export const html = '{{iframe}}'
13 | export const scriptText = `{{callback}}(new {{apiVar}}.Player({{node}}));`
14 |
--------------------------------------------------------------------------------
/src/config/apivideo.js:
--------------------------------------------------------------------------------
1 | // https://docs.api.video/docs/video-player-sdk
2 | export const type = 'iframe'
3 | export const key = 'apivideo'
4 | export const name = 'api.video'
5 | export const url = 'https://api.video'
6 | export const srcPattern = 'api\\.video/(?:videos|vod)/(\\w+)'
7 | export const embedUrl = 'https://embed.api.video/vod/{{metaId}}#{{params}}'
8 | export const pkg = '@api.video/player-sdk'
9 | export const jsUrl = '{{npmCdn}}/{{pkg}}@{{version}}/dist/index.js'
10 | export const apiVar = 'PlayerSdk'
11 | export const version = '1.2.6'
12 | export const html = '{{iframe}}';
13 | export const scriptText = `{{callback}}(new {{apiVar}}('#{{id}}'));`
14 |
--------------------------------------------------------------------------------
/src/config/dashjs.js:
--------------------------------------------------------------------------------
1 | // https://github.com/Dash-Industry-Forum/dash.js
2 | export const type = 'inline'
3 | export const key = 'dashjs'
4 | export const name = 'dash.js'
5 | export const url = 'https://github.com/Dash-Industry-Forum/dash.js'
6 | export const srcPattern = '\\.mpd($|\\?)'
7 | export const pkg = 'dashjs'
8 | export const jsUrl = '{{npmCdn}}/{{pkg}}@{{version}}/dist/dash.all.min.js'
9 | export const apiVar = 'dashjs'
10 | export const version = '3.2.2'
11 | export const html = '{{video}}'
12 | export const scriptText = `
13 | var api = {{apiVar}}.MediaPlayer().create();
14 | api.initialize({{node}}, {{src}}, {{autoplay}});
15 | {{callback}}(api);
16 | `
17 |
--------------------------------------------------------------------------------
/src/config/shakaplayer.js:
--------------------------------------------------------------------------------
1 | // https://shaka-player-demo.appspot.com/docs/api/index.html
2 | export const type = 'inline'
3 | export const key = 'shakaplayer'
4 | export const name = 'Shaka Player'
5 | export const url = 'https://github.com/google/shaka-player'
6 | export const srcPattern = '\\?player=shakaplayer'
7 | export const pkg = 'shaka-player'
8 | export const jsUrl = '{{npmCdn}}/{{pkg}}@{{version}}/dist/shaka-player.compiled.js'
9 | export const apiVar = 'shaka'
10 | export const version = '3.1.0'
11 | export const html = '{{video}}'
12 | export const scriptText = `
13 | var api = new {{apiVar}}.Player({{node}});
14 | api.load({{src}});
15 | {{callback}}(api);
16 | `
17 |
--------------------------------------------------------------------------------
/src/config/cloudflare.js:
--------------------------------------------------------------------------------
1 | // https://developers.cloudflare.com/stream/viewing-videos/using-the-player-api
2 | export const type = 'iframe'
3 | export const key = 'cloudflare'
4 | export const name = 'Cloudflare'
5 | export const url = 'https://www.cloudflare.com'
6 | export const srcPattern = '(?:cloudflarestream\\.com|videodelivery\\.net)/(\\w+)'
7 | export const embedUrl = 'https://iframe.videodelivery.net/{{metaId}}?{{params}}'
8 | export const jsUrl = 'https://embed.videodelivery.net/embed/sdk.latest.js'
9 | export const apiVar = 'Stream'
10 | export const version = '1.x.x'
11 | export const html = '{{iframe}}'
12 | export const scriptText = `{{callback}}({{apiVar}}({{node}}));`
13 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/base.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/root.njk" %}
2 |
3 | {% block head %}
4 |
5 |
6 |
7 |
8 |
9 |
10 | {% endblock %}
11 |
12 | {% block main %}
13 | {% block header %}
14 |
15 | {% include "header.njk" %}
16 |
17 | {% endblock %}
18 |
19 | {% block content %}
20 | {% endblock %}
21 |
22 | {% block footer %}
23 | {% include "footer.njk" %}
24 | {% endblock %}
25 | {% endblock %}
26 |
--------------------------------------------------------------------------------
/src/config/hlsjs.js:
--------------------------------------------------------------------------------
1 | // https://github.com/video-dev/hls.js
2 | export const type = 'inline'
3 | export const key = 'hlsjs'
4 | export const name = 'hls.js'
5 | export const url = 'https://github.com/video-dev/hls.js'
6 | export const srcPattern = '\\.m3u8($|\\?)'
7 | export const pkg = 'hls.js'
8 | export const jsUrl = '{{npmCdn}}/{{pkg}}@{{version}}/dist/hls.min.js'
9 | export const apiVar = 'Hls'
10 | export const version = '1.0.7'
11 | export const html = '{{video}}'
12 | export const scriptText = `
13 | if ({{apiVar}}.isSupported()) {
14 | var api = new {{apiVar}}({{options}});
15 | api.attachMedia({{node}});
16 | api.loadSource({{src}});
17 | {{callback}}(api);
18 | }
19 | `
20 |
--------------------------------------------------------------------------------
/site/src/utils/filters/section.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Split the content into excerpt and remainder
3 | *
4 | * @param {String} str
5 | * @param {String [excerpt | remainder]} section
6 | *
7 | * If excerpt or nothing is passed as an argument, we return what was before the split marker.
8 | * If remainder is passed as an argument, we return the rest of the post
9 | *
10 | */
11 | module.exports = function(str, section) {
12 | var content = new String(str);
13 | var delimit = '\n\n';
14 | var parts = content.split(delimit);
15 | var which = section == 'remainder' ? 1 : 0;
16 | if (parts.length) {
17 | return parts[which];
18 | } else {
19 | return str;
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/src/config/theoplayer.js:
--------------------------------------------------------------------------------
1 | // https://docs.theoplayer.com/getting-started/01-sdks/01-web/00-getting-started.md
2 | export const type = 'inline'
3 | export const key = 'theoplayer'
4 | export const name = 'THEOplayer'
5 | export const url = 'https://www.theoplayer.com'
6 | export const srcPattern = '\\?player=theoplayer'
7 | export const cssUrl = '{{libraryLocation}}/ui.css'
8 | export const jsUrl = '{{libraryLocation}}/THEOplayer.js'
9 | export const apiVar = 'THEOplayer'
10 | export const version = '1.x.x'
11 | export const html = ''
12 | export const scriptText = `
13 | {{callback}}(new {{apiVar}}.Player({{node}}, {
14 | {{libraryLocation}},
15 | {{license}},
16 | }));
17 | `
18 |
--------------------------------------------------------------------------------
/site/src/pages/players.njk:
--------------------------------------------------------------------------------
1 | ---js
2 | {
3 | eleventyComputed: {
4 | title: "Playerx - {{ player.name }}",
5 | },
6 | layout: "layouts/player.njk",
7 | tags: ["player"],
8 | pagination: {
9 | data: "players",
10 | size: 1,
11 | alias: "player",
12 | before: function(data) {
13 | const pages = [];
14 | data.forEach(entry => {
15 | entry.clips.forEach((src, i) => {
16 | pages.push({
17 | ...entry,
18 | src: src,
19 | clipIndex: i
20 | })
21 | })
22 | });
23 | return pages;
24 | }
25 | },
26 | permalink: "/players/{{ player.name | slug }}/{% if player.clipIndex > 0 %}{{ player.clipIndex + 1 }}/{% endif %}"
27 | }
28 | ---
29 |
--------------------------------------------------------------------------------
/site/src/pages/lighthouse.njk:
--------------------------------------------------------------------------------
1 | ---js
2 | {
3 | eleventyComputed: {
4 | title: "Playerx - {{ player.name }}",
5 | },
6 | layout: "layouts/lighthouse.njk",
7 | tags: ["player"],
8 | pagination: {
9 | data: "players",
10 | size: 1,
11 | alias: "player",
12 | before: function(data) {
13 | const pages = [];
14 | data.forEach(entry => {
15 | entry.clips.forEach((src, i) => {
16 | pages.push({
17 | ...entry,
18 | src: src,
19 | clipIndex: i
20 | })
21 | })
22 | });
23 | return pages;
24 | }
25 | },
26 | permalink: "/lighthouse/{{ player.name | slug }}/{% if player.clipIndex > 0 %}{{ player.clipIndex + 1 }}/{% endif %}"
27 | }
28 | ---
29 |
--------------------------------------------------------------------------------
/site/src/pages/demo-player.njk:
--------------------------------------------------------------------------------
1 | ---js
2 | {
3 | title: "Playerx - API Demo",
4 | layout: "layouts/demo-player.njk",
5 | tags: ["demo"],
6 | pagination: {
7 | data: "players",
8 | size: 1,
9 | addAllPagesToCollections: true,
10 | alias: "player",
11 | before: function(data) {
12 | const pages = [];
13 | data.forEach(entry => {
14 | pages.push(entry);
15 | entry.clips.slice(1).forEach((_, i) => {
16 | pages.push({
17 | ...entry,
18 | clipIndex: i + 1
19 | })
20 | })
21 | });
22 | return pages;
23 | }
24 | },
25 | permalink: "/demo/{{ player.name | slug }}/{% if player.clipIndex > 0 %}{{ player.clipIndex + 1 }}/{% endif %}"
26 | }
27 | ---
28 |
--------------------------------------------------------------------------------
/src/config/youtube.js:
--------------------------------------------------------------------------------
1 | // https://developers.google.com/youtube/iframe_api_reference
2 | export const type = 'iframe'
3 | export const key = 'youtube'
4 | export const name = 'YouTube'
5 | export const url = 'https://www.youtube.com'
6 | export const srcPattern = '(?:youtu\\.be/|youtube\\.com/(?:embed/|v/|watch\\?v=|watch\\?.+&v=))((\\w|-){11})'
7 | export const embedUrl = 'https://www.youtube.com/embed/{{metaId}}?{{params}}'
8 | export const jsUrl = 'https://www.youtube.com/iframe_api'
9 | export const apiVar = 'YT'
10 | export const apiReady = 'onYouTubeIframeAPIReady'
11 | export const version = '1.x.x'
12 | export const html = '{{iframe}}'
13 | export const scriptText = `
14 | function {{apiReady}}() {
15 | {{callback}}(new {{apiVar}}.Player({{node}}));
16 | }
17 | `
18 |
--------------------------------------------------------------------------------
/src/config/videojs.js:
--------------------------------------------------------------------------------
1 | export const type = 'inline'
2 | export const key = 'videojs'
3 | export const name = 'video.js'
4 | export const url = 'https://github.com/videojs/video.js'
5 | export const srcPattern = '\\?player=videojs'
6 | export const pkg = 'video.js'
7 | export const cssUrl = '{{npmCdn}}/{{pkg}}@{{version}}/dist/video-js.min.css'
8 | export const jsUrl = '{{npmCdn}}/{{pkg}}@${version}/dist/video.min.js'
9 | export const apiVar = 'videojs'
10 | export const version = '7.11.8'
11 | export const html = `
12 |
13 | `
14 | export const scriptText = `{{callback}}({{apiVar}}({{node}}));`
15 |
--------------------------------------------------------------------------------
/site/src/js/utils/dom.js:
--------------------------------------------------------------------------------
1 |
2 | export function qs(selector) {
3 | return document.querySelector(selector);
4 | }
5 |
6 | export function ready(fn) {
7 | if (document.readyState != 'loading'){
8 | fn();
9 | } else {
10 | document.addEventListener('DOMContentLoaded', fn);
11 | }
12 | }
13 |
14 | export function cx(classes) {
15 | return function() {
16 | const { el } = this;
17 | Object.keys(classes).forEach((key) => {
18 | const value = classes[key];
19 | el.classList.toggle(key, typeof value === 'function' ? value() : value);
20 | });
21 | return el.className;
22 | };
23 | }
24 |
25 | export function stopProp(fn) {
26 | return (e) => {
27 | if (e) {
28 | e.preventDefault();
29 | e.stopPropagation();
30 | }
31 | fn(e);
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/root.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ title }}
7 |
8 |
11 |
18 |
19 | {% block head %}
20 | {% endblock %}
21 |
22 | {% block body %}{% endblock %}
23 | {% block main %}
24 | {% endblock %}
25 |
26 | {% block foot %}
27 | {% endblock %}
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.github/workflows/cloudflare.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: Cloudflare Site Deploy
4 |
5 | # Controls when the action will run.
6 | on:
7 | # Allows you to run this workflow manually from the Actions tab
8 | workflow_dispatch:
9 |
10 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
11 | jobs:
12 | # This workflow contains a single job called "deploy"
13 | deploy:
14 | # The type of runner that the job will run on
15 | runs-on: ubuntu-latest
16 |
17 | # Steps represent a sequence of tasks that will be executed as part of the job
18 | steps:
19 | - name: Run Cloudflare deploy
20 | uses: fjogeleit/http-request-action@master
21 | with:
22 | url: ${{ secrets.CLOUDFLAR_DEPLOY_HOOK_URL }}
23 | method: 'POST'
24 |
--------------------------------------------------------------------------------
/site/src/pages/docs/player.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: docs/playerx/
3 | title: "Playerx - Playerx element"
4 | eleventyNavigation:
5 | key:
6 | parent: elements
7 | order: 1
8 | tags:
9 | - docs
10 | layout: layouts/docs.njk
11 | ---
12 |
13 | # Playerx Element
14 |
15 | The `` element embeds any media player in the document and provides a **uniform API** to allow you to control those video and audio players programmatically. The `Playerx` API mimics the `HTMLMediaElement` API as closely as possible.
16 |
17 |
20 |
21 | ```html
22 |
23 | ```
24 |
--------------------------------------------------------------------------------
/src/config/wistia.js:
--------------------------------------------------------------------------------
1 | // https://wistia.com/support/developers/player-api
2 | export const type = 'inline'
3 | export const key = 'wistia'
4 | export const name = 'Wistia'
5 | export const url = 'https://wistia.com'
6 | export const srcPattern = '(?:wistia\\.com|wi\\.st)/(?:medias|embed)/(.*)$'
7 | export const embedUrl = 'https://fast.wistia.net/embed/iframe/{{metaId}}'
8 | export const jsUrl = 'https://fast.wistia.com/assets/external/E-v1.js'
9 | export const apiVar = 'Wistia'
10 | export const version = '2.x.x'
11 | export const html = '{{iframe}}'
12 | export const scriptText = `
13 | window._wq.push({
14 | id: '{{metaId}}',
15 | options: {{options}},
16 | onReady: function(api) {
17 | {{callback}}(api);
18 | }
19 | });
20 | `
21 | export const setup = `
22 |
23 | {{js}}
24 | {{script}}
25 | `
26 |
--------------------------------------------------------------------------------
/site/src/utils/pipes.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const util = require('util');
3 | const exec = util.promisify(require('child_process').exec);
4 |
5 | const defaults = {
6 | input: '.',
7 | output: '_dest'
8 | };
9 |
10 | module.exports = {
11 | configFunction: (eleventyConfig, options) => {
12 | const { input, output, pipes } = Object.assign(defaults, options);
13 |
14 | for (let dir in pipes) {
15 | eleventyConfig.addWatchTarget(path.join(input, dir));
16 | }
17 |
18 | eleventyConfig.addTransform('pipes', async (content) => {
19 | await Promise.all(
20 | Object.values(pipes)
21 | .map((pipe) => exec(pipe))
22 | );
23 | return content;
24 | });
25 |
26 | eleventyConfig.setBrowserSyncConfig({
27 | files: Object.keys(pipes)
28 | .map((dir) => path.join(output, dir))
29 | });
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/src/config/index.js:
--------------------------------------------------------------------------------
1 | export * from './helpers.js'
2 |
3 | export * as apivideo from './apivideo.js'
4 | export * as brightcove from './brightcove.js'
5 | export * as cloudflare from './cloudflare.js'
6 | export * as cloudinary from './cloudinary.js'
7 | export * as dailymotion from './dailymotion.js'
8 | export * as dashjs from './dashjs.js'
9 | export * as facebook from './facebook.js'
10 | export * as hlsjs from './hlsjs.js'
11 | export * as html from './html.js'
12 | export * as jwplayer from './jwplayer.js'
13 | export * as muxvideo from './muxvideo.js'
14 | export * as shakaplayer from './shakaplayer.js'
15 | export * as streamable from './streamable.js'
16 | export * as theoplayer from './theoplayer.js'
17 | export * as videojs from './videojs.js'
18 | export * as vidyard from './vidyard.js'
19 | export * as vimeo from './vimeo.js'
20 | export * as wistia from './wistia.js'
21 | export * as youtube from './youtube.js'
22 |
--------------------------------------------------------------------------------
/src/config/jwplayer.js:
--------------------------------------------------------------------------------
1 | // https://developer.jwplayer.com/jwplayer/docs/jw8-javascript-api-reference
2 | export const type = 'inline'
3 | export const key = 'jwplayer'
4 | export const name = 'JWPlayer'
5 | export const url = 'https://www.jwplayer.com'
6 | export const srcPattern = 'jwplayer\\.com/players/(\\w+)(?:-(\\w+))?'
7 | export const embedUrl = 'https://cdn.jwplayer.com/players/{{metaId}}-{{2}}.html'
8 | export const jsUrl = 'https://content.jwplatform.com/libraries/{{2}}.js'
9 | export const apiVar = 'jwplayer'
10 | export const version = '8.12.5'
11 | export const html = ''
12 | export const scriptText = `
13 | fetch('https://cdn.jwplayer.com/v2/media/{{metaId}}')
14 | .then(function(response) { return response.json(); })
15 | .then(function(config) {
16 | {{callback}}({{apiVar}}({{node}}).setup(
17 | Object.assign(config, {{options}})
18 | ));
19 | });
20 | `
21 |
--------------------------------------------------------------------------------
/src/config/brightcove.js:
--------------------------------------------------------------------------------
1 | // https://player.support.brightcove.com/coding-topics/overview-player-api.html
2 | export const type = 'inline';
3 | export const key = 'brightcove';
4 | export const name = 'Brightcove';
5 | export const url = 'https://www.brightcove.com';
6 | export const srcPattern =
7 | 'players\\.brightcove\\.net/(\\d+)/(\\w+)_(\\w+)/.*?videoId=(\\d+)';
8 | export const metaId = '{{4}}';
9 | export const jsUrl =
10 | 'https://players.brightcove.net/{{1}}/default_default/index.min.js';
11 | export const apiVar = 'bc';
12 | export const version = '1.x.x';
13 | export const html = ``;
14 | export const scriptText = `{{callback}}({{apiVar}}({{node}}));`;
15 |
--------------------------------------------------------------------------------
/site/src/js/routing.js:
--------------------------------------------------------------------------------
1 | import { on } from 'sinuous/observable';
2 | import { defaults, autoplay, muted, loop, controls, src } from './demo.js';
3 | import { getParams, toQuery } from './utils/url.js';
4 | import { qs } from './utils/utils.js';
5 |
6 | on([autoplay, muted, loop, controls, src], () => {
7 |
8 | const btn = qs(`[data-src='${JSON.stringify(src())}']`);
9 | const options = {
10 | ...getParams(),
11 | autoplay,
12 | muted,
13 | loop,
14 | controls,
15 | src: btn ? undefined : src,
16 | };
17 |
18 | let search = toQuery(options, defaults);
19 | let url;
20 | if (btn) {
21 | if (btn.dataset.clip === '1') {
22 | url = `/demo/${btn.dataset.player}/${search}`;
23 | } else {
24 | url = `/demo/${btn.dataset.player}/${btn.dataset.clip}/${search}`;
25 | }
26 | } else {
27 | url = `/demo/${search}`;
28 | }
29 |
30 | history.pushState({}, '', url);
31 |
32 | }, null, true);
33 |
--------------------------------------------------------------------------------
/src/config/vidyard.js:
--------------------------------------------------------------------------------
1 | // https://knowledge.vidyard.com/hc/en-us/articles/360019034753
2 | export const type = 'iframe'
3 | export const key = 'vidyard'
4 | export const name = 'Vidyard'
5 | export const url = 'https://www.vidyard.com'
6 | export const srcPattern = 'vidyard\\.com/(?:share|watch/)?(\\w+)'
7 | export const embedUrl = 'https://play.vidyard.com/{{metaId}}?{{params}}'
8 | export const jsUrl = 'https://play.vidyard.com/embed/v{{version}}.js'
9 | export const apiVar = 'VidyardV4'
10 | export const apiReady = 'onVidyardAPI'
11 | export const version = '4'
12 | export const html = '{{iframe}}'
13 | export const scriptText = `
14 | function {{apiReady}}() {
15 | {{apiVar}}.api.renderPlayer(Object.assign({
16 | uuid: '{{metaId}}',
17 | container: {{node}}
18 | }, {{options}}))
19 | .then(function(api) {
20 | {{callback}}(api);
21 | });
22 | }
23 | `
24 | export const setup = `
25 |
26 | {{js}}
27 | {{script}}
28 | `
29 |
--------------------------------------------------------------------------------
/src/config/dailymotion.js:
--------------------------------------------------------------------------------
1 | // https://developer.dailymotion.com/player/
2 | export const type = 'iframe'
3 | export const key = 'dailymotion'
4 | export const name = 'Dailymotion'
5 | export const url = 'https://www.dailymotion.com'
6 | export const srcPattern = '(?:(?:dailymotion\\.com(?:/embed)?/video)|dai\\.ly)/(\\w+)$'
7 | export const embedUrl = 'https://www.dailymotion.com/embed/video/{{metaId}}?{{params}}'
8 | export const jsUrl = 'https://api.dmcdn.net/all.js'
9 | export const apiVar = 'DM'
10 | export const apiReady = 'dmAsyncInit'
11 | export const version = '1.x.x'
12 | export const html = '{{iframe}}'
13 | export const scriptText = `
14 | function {{apiReady}}() {
15 | var api = {{apiVar}}.player({{node}}, {
16 | video: '{{metaId}}',
17 | params: {{options}},
18 | width: '{{width}}',
19 | height: '{{height}}',
20 | });
21 | api.allow = '{{allow}}';
22 | {{callback}}(api);
23 | }
24 | `
25 | export const setup = `
26 |
27 | {{js}}
28 | {{script}}
29 | `
30 |
--------------------------------------------------------------------------------
/site/src/utils/filters/squash.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Make a search index string by removing duplicated words
3 | * and removing less useful, common short words
4 | *
5 | * @param {String} text
6 | */
7 |
8 | module.exports = function(text) {
9 | var content = new String(text);
10 |
11 | // remove all html elements
12 | var re = /(<.+?>)/gi;
13 | var plain = content.replace(re, '');
14 | re = /(&.+?;)/gi;
15 | plain = plain.replace(re, '');
16 |
17 | // remove duplicated words
18 | var words = plain.split(' ');
19 | var deduped = [...new Set(words)];
20 | var dedupedStr = deduped.join(' ');
21 |
22 | // remove short and less meaningful words
23 | var result = dedupedStr.replace(
24 | /\b(\.|,|the|a|an|and|am|all|you|I|to|if|of|off|me|my|on|in|it|is|at|as|we|do|be|has|but|was|so|no|not|or|up|for)\b/gi,
25 | ''
26 | );
27 | //remove newlines, and punctuation
28 | result = result.replace(/\.|,|\?|-|—|\n/g, '');
29 | //remove repeated spaces
30 | result = result.replace(/[ ]{2,}/g, ' ');
31 |
32 | return result;
33 | };
34 |
--------------------------------------------------------------------------------
/site/src/js/utils/url.js:
--------------------------------------------------------------------------------
1 | export function getParam(key, defaultValue) {
2 | const params = new URLSearchParams(location.search);
3 | return params.has(key)
4 | ? params.get(key) === '1'
5 | ? true
6 | : params.get(key) === '0'
7 | ? false
8 | : params.get(key)
9 | : defaultValue;
10 | }
11 |
12 | export function getParams() {
13 | return Object.fromEntries(new URLSearchParams(location.search));
14 | }
15 |
16 | export function toParams(obj, defaults) {
17 | const values = {};
18 | for (let key in obj) {
19 | let value = typeof obj[key] === 'function' ? obj[key]() : obj[key];
20 | if (typeof defaults[key] === 'boolean') value = !!value;
21 | if (value == defaults[key]) continue;
22 |
23 | if (value === undefined) delete values[key];
24 | else if (value === true) values[key] = 1;
25 | else if (!value) values[key] = 0;
26 | else values[key] = value;
27 | }
28 | return new URLSearchParams(values);
29 | }
30 |
31 | export function toQuery(obj, defaults) {
32 | const params = toParams(obj, defaults).toString();
33 | return params ? '?' + params : '';
34 | }
35 |
--------------------------------------------------------------------------------
/site/src/css/base.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-size: 14px;
3 | }
4 |
5 | @media (min-width: 420px) {
6 | html {
7 | font-size: 16px;
8 | }
9 | }
10 |
11 | a {
12 | color: inherit;
13 | text-decoration: none;
14 | }
15 |
16 | body:not(.showfocus) button,
17 | input[type="range"] {
18 | outline: none;
19 | }
20 |
21 | [hidden] {
22 | display: none !important
23 | }
24 |
25 | .focus\:outline-0:focus {
26 | outline: 0!important
27 | }
28 |
29 | ::selection {
30 | background: #AEFCE2;
31 | }
32 |
33 | .logo {
34 | width: 124px;
35 | height: 32px;
36 | }
37 |
38 | .menu {
39 | @apply mb-8;
40 | }
41 |
42 | .menu-label {
43 | @apply text-gray-600 text-xs uppercase tracking-widest
44 | }
45 |
46 | .menu-label:not(:last-child) {
47 | @apply mb-4
48 | }
49 |
50 | .menu-label:not(:first-child) {
51 | @apply mt-4
52 | }
53 |
54 | .menu-list {
55 | @apply leading-tight
56 | }
57 |
58 | .menu-list a {
59 | @apply block rounded-sm py-2 px-3 text-gray-700
60 | }
61 |
62 | .menu-list a:hover {
63 | @apply text-gray-900 bg-gray-100
64 | }
65 |
66 | .menu-list a.is-active {
67 | @apply text-white bg-blue-600
68 | }
69 |
--------------------------------------------------------------------------------
/site/src/css/home.css:
--------------------------------------------------------------------------------
1 | #main-menu.active {
2 | @apply flex;
3 | }
4 |
5 | #burger.active svg:first-child {
6 | display: none;
7 | }
8 |
9 | #burger.active svg:last-child {
10 | display: block;
11 | }
12 |
13 | .highlight-clear pre[class*="language-"] {
14 | margin: 0;
15 | padding: 0;
16 | background: none;
17 | overflow: hidden;
18 | }
19 |
20 | .buy-button {
21 | @apply block mt-4 w-full text-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded transition duration-150 ease-in-out;
22 | }
23 |
24 | .buy-button:active {
25 | @apply bg-aquamarine-700;
26 | }
27 |
28 | .buy-button:focus {
29 | @apply outline-none ring-aquamarine-300 border-aquamarine-700;
30 | }
31 |
32 | .cell {
33 | @apply flex-grow overflow-hidden w-full border-b p-5;
34 | }
35 |
36 | .feature-list {
37 | @apply text-sm;
38 | }
39 |
40 | .feature-list li {
41 | @apply relative pl-4 py-1;
42 | }
43 |
44 | .feature-list li:before {
45 | content: '•';
46 | @apply absolute text-gray-300 left-0;
47 | }
48 |
49 | .menu-options {
50 | @apply origin-top-right absolute right-0 mt-2 w-24 rounded-md shadow-lg;
51 | }
52 |
--------------------------------------------------------------------------------
/site/src/css/player.css:
--------------------------------------------------------------------------------
1 | /* preview + play button */
2 | player-x .plx-preview {
3 | transition: all 0.5s cubic-bezier(0, 0, 0.2, 1);
4 | }
5 |
6 | player-x .plx-playbtn {
7 | font-size: 10px;
8 | width: 6.5em;
9 | height: 4em;
10 | background: rgba(23, 35, 34, .75);
11 | z-index: 1;
12 | opacity: 0.8;
13 | /* border-radius: .5em; */
14 | transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
15 | outline: 0;
16 | border: 0;
17 | cursor: pointer;
18 | }
19 |
20 | .plx-playbtn:hover {
21 | /* background-color: rgb(0, 173, 239); */
22 | opacity: 1;
23 | }
24 |
25 | /* play button triangle */
26 | player-x .plx-playbtn::before {
27 | content: '';
28 | border-style: solid;
29 | border-width: 10px 0 10px 20px;
30 | border-color: transparent transparent transparent #fff;
31 | }
32 |
33 | player-x .plx-playbtn,
34 | player-x .plx-playbtn::before {
35 | position: absolute;
36 | top: 50%;
37 | left: 50%;
38 | transform: translate3d(-50%, -50%, 0);
39 | }
40 |
41 | player-x .plx-playbtn::before {
42 | left: calc(50% + 1px);
43 | }
44 |
45 | /* Post-click styles */
46 | player-x:not([loading]) {
47 | cursor: unset;
48 | }
49 |
50 | player-x:not([loading]) .plx-preview {
51 | opacity: 0;
52 | pointer-events: none;
53 | }
54 |
--------------------------------------------------------------------------------
/src/config/cloudinary.js:
--------------------------------------------------------------------------------
1 | // https://cloudinary.com/documentation/video_player_how_to_embed
2 | export const type = 'inline'
3 | export const key = 'cloudinary'
4 | export const name = 'Cloudinary'
5 | export const url = 'https://cloudinary.com'
6 | export const srcPattern = '(?:cloudinary\\.com)/(\\w+)/video/upload/sp_([^,/]+).*?/([^.?/]+)\\.'
7 | export const pkg = 'cloudinary-video-player'
8 | export const cssUrl = '{{npmCdn}}/{{pkg}}@{{version}}/dist/cld-video-player.min.css'
9 | export const jsUrl = '{{npmCdn}}/{{pkg}}@{{version}}/dist/cld-video-player.min.js'
10 | export const apiVar = 'cloudinary'
11 | export const version = '1.5.9'
12 | export const html = ''
13 | export const scriptText = `
14 | var cld = {{apiVar}}.Cloudinary.new({ cloud_name: '{{1}}' });
15 | {{callback}}(cld.videoPlayer('{{id}}', {
16 | publicId: '{{3}}',
17 | sourceTypes: ['hls', 'dash', 'mp4'],
18 | transformation: {
19 | streaming_profile: '{{2}}',
20 | },
21 | }));
22 | `
23 | export const setup = `
24 | {{html}}
25 | {{css}}
26 |
27 | {{js}}
28 | {{script}}
29 | `
30 |
--------------------------------------------------------------------------------
/src/config/facebook.js:
--------------------------------------------------------------------------------
1 | // https://developers.facebook.com/docs/plugins/embedded-video-player/api/
2 | export const type = 'iframe'
3 | export const key = 'facebook'
4 | export const name = 'Facebook'
5 | export const url = 'https://www.facebook.com'
6 | export const srcPattern = 'facebook\\.com/.*videos(/|%2F)(\\d+)'
7 | export const allowfullscreen = 'true'
8 | export const embedUrl = 'https://www.facebook.com/v3.2/plugins/video.php?href={{src}}&allowfullscreen={{allowfullscreen}}&{{params}}'
9 | export const jsUrl = 'https://connect.facebook.net/en_US/sdk.js'
10 | export const apiVar = 'FB'
11 | export const apiReady = 'fbAsyncInit'
12 | export const version = '3.2'
13 | export const html = '{{iframe}}'
14 | export const scriptText = `
15 | function {{apiReady}}() {
16 | {{apiVar}}.init({
17 | appId: '{{appId}}',
18 | version: 'v{{version}}',
19 | xfbml: true,
20 | });
21 |
22 | {{apiVar}}.Event.subscribe('xfbml.ready', msg => {
23 | if (msg.type === 'video' && msg.id === '{{id}}') {
24 | {{node}}.querySelector('iframe').allow = '{{allow}}';
25 | {{callback}}(msg.instance);
26 | }
27 | });
28 | }
29 | `
30 | export const setup = `
31 |
32 | {{js}}
33 | {{script}}
34 | `
35 |
--------------------------------------------------------------------------------
/site/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const { fontFamily } = require('tailwindcss/defaultTheme');
2 | const colors = require('tailwindcss/colors')
3 |
4 | const aquamarine = {
5 | 50: '#E9FCF7',
6 | 100: '#CBF8EC',
7 | 200: '#9DF3DB',
8 | 300: '#8EF1D5',
9 | 400: '#70EDCA',
10 | 500: '#4ADEC0',
11 | 600: '#18BD9F',
12 | 700: '#129083',
13 | 800: '#0B544E',
14 | 900: '#073628',
15 | };
16 |
17 | module.exports = {
18 | content: ['./src/**/*.{html,njk,md,js}'],
19 | safelist: [
20 | 'grid-cols-2',
21 | 'grid-cols-3',
22 | 'grid-cols-4',
23 | 'grid-cols-5',
24 | 'category_performance_median',
25 | ],
26 | theme: {
27 | extend: {
28 | fontFamily: {
29 | sans: ['Inter var', ...fontFamily.sans],
30 | source: ['Source Sans Pro', ...fontFamily.sans],
31 | system: fontFamily.sans,
32 | },
33 | colors: {
34 | aquamarine,
35 | gray: colors.slate,
36 | },
37 | width: {
38 | '1/9': '11.111111111%',
39 | '2/9': '22.222222222%',
40 | '3/9': '33.333333333%',
41 | '4/9': '44.444444444%',
42 | '5/9': '55.555555556%',
43 | '6/9': '66.666666667%',
44 | '7/9': '77.777777778%',
45 | '8/9': '88.888888889%',
46 | },
47 | inset: {
48 | '-4': '-1rem',
49 | '-6': '-1.5rem',
50 | '-8': '-2rem',
51 | '-12': '-3rem',
52 | }
53 | }
54 | },
55 | plugins: [
56 | require('@tailwindcss/typography'),
57 | ]
58 | };
59 |
--------------------------------------------------------------------------------
/site/src/css/demo.css:
--------------------------------------------------------------------------------
1 |
2 | input[type="range"]::-webkit-slider-thumb {
3 | -webkit-appearance: none;
4 | height: 13px;
5 | width: 13px;
6 | border-radius: 50%;
7 | background: #67758A;
8 | cursor: pointer;
9 | }
10 |
11 | input[type="range"]::-moz-range-thumb {
12 | height: 13px;
13 | width: 13px;
14 | border-radius: 50%;
15 | background: #67758A;
16 | cursor: pointer;
17 | }
18 |
19 | input[type="range"]::-ms-thumb {
20 | height: 13px;
21 | width: 13px;
22 | border-radius: 50%;
23 | background: #67758A;
24 | cursor: pointer;
25 | }
26 |
27 | player-x {
28 | display: block;
29 | width: 100%;
30 | aspect-ratio: 16 / 9;
31 | }
32 |
33 | .btn {
34 | @apply bg-gray-400 text-white text-sm py-1 px-2 rounded mr-1;
35 | }
36 |
37 | .btn-active {
38 | @apply bg-gray-500 shadow-inner;
39 | }
40 |
41 | .btn:hover:enabled {
42 | @apply bg-gray-600;
43 | }
44 |
45 | .src-btn {
46 | @apply leading-tight;
47 | }
48 |
49 | .pill {
50 | @apply bg-aquamarine-100 opacity-50 transition-opacity text-aquamarine-600 text-xs py-1 px-3 rounded-full mr-2;
51 | }
52 |
53 | .pill.pill-on {
54 | @apply opacity-100;
55 | }
56 |
57 | .pill > i {
58 | @apply font-bold font-mono text-aquamarine-700;
59 | }
60 |
61 | .log:first-child {
62 | border-top: 0;
63 | }
64 |
65 | .log {
66 | @apply px-2 border-t border-gray-200 flex justify-between;
67 | }
68 |
69 | .log > div {
70 | @apply truncate;
71 | width: calc(100% - 3rem);
72 | }
73 |
74 | .log > i {
75 | @apply text-gray-400;
76 | }
77 |
--------------------------------------------------------------------------------
/site/src/js/helpers/dropdown.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'sinuous';
2 | import { stopProp } from '../utils/dom.js';
3 | import { invert } from '../utils/utils.js';
4 |
5 | export function dropdown() {
6 | const isOpen = observable(false);
7 | const isHidden = observable(true);
8 | const toggle = stopProp(invert(isOpen));
9 | const open = stopProp(() => isOpen(true));
10 | const close = stopProp(() => isOpen(false));
11 | let transitioning = false;
12 |
13 | function classes() {
14 | const dataset = this.el.dataset;
15 | const type = `transition:${isOpen() ? 'enter' : 'leave'}`;
16 |
17 | if (!transitioning) {
18 | transitioning = true;
19 |
20 | requestAnimationFrame(() => {
21 | // 2. Show the element after one tick.
22 | if (isOpen()) isHidden(false);
23 | // 3. Continues below...
24 | requestAnimationFrame(() => isOpen(isOpen()));
25 | });
26 |
27 | // 1. First set the start CSS classes.
28 | return `${dataset[type]} ${dataset[`${type}Start`]}`;
29 | }
30 |
31 | // 3. Lastly set the end CSS classes and hide the element on transition end.
32 | this.el.addEventListener('transitionend', onTransitionEnd, { once: true });
33 | function onTransitionEnd() {
34 | if (!isOpen()) isHidden(true);
35 | }
36 |
37 | transitioning = false;
38 | return `${dataset[type]} ${dataset[`${type}End`]}`;
39 | }
40 |
41 | return Object.assign(classes, {
42 | toggle,
43 | open,
44 | close,
45 | isHidden,
46 | isOpen,
47 | });
48 | }
49 |
--------------------------------------------------------------------------------
/site/src/js/utils/utils.js:
--------------------------------------------------------------------------------
1 | import { observable, subscribe, sample } from 'sinuous/observable';
2 |
3 | export const qs = (selector) => document.querySelector(selector);
4 |
5 | export function invert(accessor) {
6 | return () => accessor(!accessor());
7 | }
8 |
9 | export function computedValue(fn) {
10 | let val = observable(fn());
11 | subscribe(() => {
12 | if (sample(val) !== fn()) {
13 | val(fn());
14 | }
15 | });
16 | return val;
17 | }
18 |
19 | export function value(current) {
20 | const v = observable(current);
21 | return function (update) {
22 | if (!arguments.length) return v();
23 | if (update !== current) {
24 | current = v(update);
25 | }
26 | return update;
27 | };
28 | }
29 |
30 | export function toHHMMSS(secs) {
31 | const sec_num = parseInt(secs, 10),
32 | hours = Math.floor(sec_num / 3600),
33 | minutes = Math.floor(sec_num / 60) % 60,
34 | seconds = sec_num % 60;
35 |
36 | return [hours, minutes, seconds]
37 | .map((v) => (v < 10 ? '0' + v : v))
38 | .filter((v, i) => v !== '00' || i > 0)
39 | .join(':');
40 | }
41 |
42 | export function round(num, precision) {
43 | return +(Math.round(num + 'e+' + precision) + 'e-' + precision);
44 | }
45 |
46 | export function prettyQuality(height) {
47 | if (!height) return 'n/a';
48 | if (height >= 2160) return '4K';
49 | if (height >= 1440) return '2K';
50 | return `${height}p`;
51 | }
52 |
53 | export function tryJSONParse(input) {
54 | try {
55 | input = JSON.parse(input);
56 | } catch (err) {/**/}
57 | return input;
58 | }
59 |
--------------------------------------------------------------------------------
/site/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import { nodeResolve } from '@rollup/plugin-node-resolve';
2 | import babel from 'rollup-plugin-babel';
3 | import terser from '@rollup/plugin-terser';
4 | import bundleSize from 'rollup-plugin-size';
5 | import sourcemaps from 'rollup-plugin-sourcemaps';
6 |
7 | const production = !process.env.ROLLUP_WATCH;
8 |
9 | const terserPlugin = terser({
10 | warnings: true,
11 | compress: {
12 | passes: 2,
13 | drop_console: production,
14 | sequences: false, // caused an issue with Babel where sequence order was wrong
15 | }
16 | });
17 |
18 | const config = {
19 | input: 'src/js/index.js',
20 | watch: {
21 | clearScreen: false
22 | },
23 | output: {
24 | format: 'iife',
25 | sourcemap: true,
26 | file: 'public/js/playerx-demo.js',
27 | name: 'playerxDemo',
28 | },
29 | plugins: [
30 | bundleSize(),
31 | sourcemaps(),
32 | nodeResolve(),
33 |
34 | babel({
35 | inputSourceMap: false,
36 | compact: false,
37 | }),
38 |
39 | terserPlugin
40 | ]
41 | };
42 |
43 | export default [
44 | config,
45 | {
46 | ...config,
47 | input: 'src/js/site.js',
48 | output: {
49 | ...config.output,
50 | file: 'public/js/site.js',
51 | name: 'site',
52 | },
53 | },
54 | {
55 | ...config,
56 | input: 'src/js/matrix.js',
57 | output: {
58 | ...config.output,
59 | file: 'public/js/matrix.js',
60 | name: 'matrix',
61 | },
62 | },
63 | // {
64 | // ...config,
65 | // input: 'src/js/compare.js',
66 | // output: {
67 | // ...config.output,
68 | // file: 'public/js/compare.js',
69 | // name: 'compare',
70 | // }
71 | // },
72 | // {
73 | // ...config,
74 | // input: 'src/js/rendition-observer.js',
75 | // output: {
76 | // ...config.output,
77 | // file: 'public/js/rendition-observer.js',
78 | // name: 'renditionObserver',
79 | // },
80 | // },
81 | ];
82 |
--------------------------------------------------------------------------------
/src/options.js:
--------------------------------------------------------------------------------
1 | export const options = {
2 | npmCdn: 'https://cdn.jsdelivr.net/npm',
3 | players: {
4 | html: {
5 | type: 'video',
6 | },
7 | hls: {
8 | pattern: /\.m3u8($|\?)/i,
9 | type: 'hls-video',
10 | pkg: 'hls-video-element',
11 | version: '1.1',
12 | jsUrl: '{{npmCdn}}/{{pkg}}@{{version}}/+esm',
13 | },
14 | dash: {
15 | pattern: /\.mpd($|\?)/i,
16 | type: 'dash-video',
17 | pkg: '@luwes/dash-video-element',
18 | version: '1.0',
19 | jsUrl: '{{npmCdn}}/{{pkg}}@{{version}}/+esm',
20 | },
21 | muxplayer: {
22 | pattern: /stream\.mux\.com\/(\w+)|\?player=muxplayer/,
23 | type: 'mux-player',
24 | pkg: '@mux/mux-player',
25 | version: '2.5',
26 | jsUrl: '{{npmCdn}}/{{pkg}}@{{version}}/dist/mux-player.js',
27 | },
28 | jwplayer: {
29 | pattern: /jwplayer\.com\/players\/(\w+)(?:-(\w+))?/,
30 | type: 'jwplayer-video',
31 | pkg: 'jwplayer-video-element',
32 | version: '1.0',
33 | jsUrl: '{{npmCdn}}/{{pkg}}@{{version}}/+esm',
34 | },
35 | vimeo: {
36 | pattern: /vimeo\.com\/(?:video\/)?(\d+)/,
37 | type: 'vimeo-video',
38 | pkg: 'vimeo-video-element',
39 | version: '1.0',
40 | jsUrl: '{{npmCdn}}/{{pkg}}@{{version}}/+esm',
41 | // jsUrl: 'http://127.0.0.1:8000/dist/vimeo-video-element.js',
42 | },
43 | youtube: {
44 | pattern: /(?:youtu\.be\/|youtube\.com\/(?:shorts\/|embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})/,
45 | type: 'youtube-video',
46 | pkg: 'youtube-video-element',
47 | version: '1.0',
48 | jsUrl: '{{npmCdn}}/{{pkg}}@{{version}}/+esm',
49 | },
50 | wistia: {
51 | pattern: /(?:wistia\.com|wi\.st)\/(?:medias|embed)\/(.*)$/,
52 | type: 'wistia-video',
53 | pkg: 'wistia-video-element',
54 | version: '1.0',
55 | jsUrl: '{{npmCdn}}/{{pkg}}@{{version}}/+esm',
56 | // jsUrl: 'http://127.0.0.1:8002/wistia-video-element.js',
57 | },
58 | },
59 | };
60 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playerx",
3 | "version": "1.0.6",
4 | "description": "Playerx",
5 | "author": "Wesley Luyten (https://wesleyluyten.com)",
6 | "license": "MIT",
7 | "repository": "luwes/playerx",
8 | "homepage": "https://github.com/luwes/playerx#readme",
9 | "bugs": {
10 | "url": "https://github.com/luwes/playerx/issues"
11 | },
12 | "type": "module",
13 | "main": "src/playerx.js",
14 | "types": "src/playerx.d.ts",
15 | "exports": {
16 | ".": "./src/playerx.js",
17 | "./config": "./src/config/index.js"
18 | },
19 | "files": [
20 | "src"
21 | ],
22 | "workspaces": [
23 | "site"
24 | ],
25 | "scripts": {
26 | "build": "npm run build --workspace=site",
27 | "dev": "npx serve --cors -p 8000 src & npm run dev --workspace=site",
28 | "serve": "npm run serve --workspace=site",
29 | "lint": "eslint src/*/",
30 | "test": "npm run lint"
31 | },
32 | "dependencies": {
33 | "@luwes/dash-video-element": "^1.0.1",
34 | "@mux/mux-player": "^2.5.0",
35 | "hls-video-element": "^1.1.4",
36 | "jwplayer-video-element": "^1.0.6",
37 | "super-media-element": "~1.3.0",
38 | "vimeo-video-element": "^1.0.3",
39 | "wistia-video-element": "^1.0.9",
40 | "youtube-video-element": "^1.0.1"
41 | },
42 | "devDependencies": {
43 | "eslint": "^8.49.0",
44 | "schema-dts": "^1.1.2",
45 | "typescript": "^5.4.5"
46 | },
47 | "prettier": {
48 | "tabWidth": 2,
49 | "singleQuote": true,
50 | "semi": true
51 | },
52 | "eslintConfig": {
53 | "root": true,
54 | "globals": {
55 | "globalThis": "writable"
56 | },
57 | "env": {
58 | "browser": true,
59 | "es6": true,
60 | "node": true
61 | },
62 | "extends": [
63 | "eslint:recommended",
64 | "plugin:import/warnings"
65 | ],
66 | "parserOptions": {
67 | "ecmaVersion": 2022,
68 | "sourceType": "module"
69 | },
70 | "rules": {
71 | "no-shadow": "error"
72 | },
73 | "ignorePatterns": [
74 | "site/src/js/ga.js"
75 | ]
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/site/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "site",
3 | "version": "1.0.0",
4 | "keywords": [],
5 | "homepage": "https://github.com/luwes/playerx#readme",
6 | "bugs": {
7 | "url": "https://github.com/luwes/playerx/issues"
8 | },
9 | "repository": "luwes/playerx",
10 | "license": "MIT",
11 | "author": "Wesley Luyten (https://wesleyluyten.com)",
12 | "main": "dist/playerx-demo.js",
13 | "module": "module/playerx-demo.js",
14 | "files": [
15 | "dist",
16 | "src"
17 | ],
18 | "scripts": {
19 | "build": "cross-env NODE_ENV=prod run-s css:prod js:prod eleventy",
20 | "dev": "cross-env NODE_ENV=dev run-p css:dev js:dev serve",
21 | "js:dev": "rollup -wc --silent",
22 | "js:prod": "rollup -c --silent",
23 | "css:prod": "tailwindcss -i src/css/main.css -o public/css/playerx-dev.css",
24 | "css:dev": "tailwindcss -i src/css/main.css -o public/css/playerx-dev.css -w",
25 | "lint": "eslint src/js",
26 | "serve": "eleventy --serve --quiet",
27 | "eleventy": "eleventy"
28 | },
29 | "dependencies": {
30 | "disco": "1.0.2",
31 | "playerx": "~1.0.6",
32 | "sinuous": "0.32.1"
33 | },
34 | "devDependencies": {
35 | "@11ty/eleventy": "^2.0.1",
36 | "@11ty/eleventy-cache-assets": "^2.3.0",
37 | "@11ty/eleventy-navigation": "^0.3.5",
38 | "@11ty/eleventy-plugin-rss": "^1.2.0",
39 | "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
40 | "@babel/core": "^7.24.4",
41 | "@babel/preset-env": "^7.24.4",
42 | "@rollup/plugin-babel": "^6.0.4",
43 | "@rollup/plugin-node-resolve": "^15.2.3",
44 | "@rollup/plugin-terser": "^0.4.4",
45 | "@tailwindcss/typography": "^0.5.12",
46 | "cross-env": "^7.0.3",
47 | "dotenv": "^16.4.5",
48 | "eslint": "^8.57.0",
49 | "eslint-plugin-import": "^2.29.1",
50 | "html-minifier": "^4.0.0",
51 | "js-yaml": "^4.1.0",
52 | "luxon": "^3.4.4",
53 | "npm-run-all": "^4.1.5",
54 | "prettier": "^3.2.5",
55 | "rollup": "^4.16.1",
56 | "rollup-plugin-babel": "4.4.0",
57 | "rollup-plugin-size": "^0.3.1",
58 | "rollup-plugin-sourcemaps": "^0.6.3",
59 | "tailwindcss": "3.4.3"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/site/src/css/prism-ghcolors.css:
--------------------------------------------------------------------------------
1 | /**
2 | * GHColors theme by Avi Aryan (http://aviaryan.in)
3 | * Inspired by Github syntax coloring
4 | */
5 |
6 | code[class*="language-"],
7 | pre[class*="language-"] {
8 | @apply font-mono;
9 |
10 | direction: ltr;
11 | text-align: left;
12 | white-space: pre;
13 | word-spacing: normal;
14 | word-break: normal;
15 | line-height: 1.5;
16 |
17 | -moz-tab-size: 4;
18 | -o-tab-size: 4;
19 | tab-size: 4;
20 |
21 | -webkit-hyphens: none;
22 | -moz-hyphens: none;
23 | -ms-hyphens: none;
24 | hyphens: none;
25 | }
26 |
27 | pre > code[class*="language-"] {
28 | font-size: 1em;
29 | }
30 |
31 | /* Code blocks */
32 | pre[class*="language-"] {
33 | @apply p-4;
34 | /* padding: 1em; */
35 | /* margin: .5em 0; */
36 | overflow: auto;
37 | background-color: #f6f8fa;
38 | border-radius: 6px;
39 | }
40 |
41 | /* Inline code */
42 | :not(pre) > code[class*="language-"] {
43 | padding: .2em;
44 | padding-top: 1px;
45 | padding-bottom: 1px;
46 | background: #f8f8f8;
47 | border: 1px solid #dddddd;
48 | }
49 |
50 | .token.comment,
51 | .token.prolog,
52 | .token.doctype,
53 | .token.cdata {
54 | color: #999988;
55 | font-style: italic;
56 | }
57 |
58 | .token.namespace {
59 | opacity: .7;
60 | }
61 |
62 | .token.string,
63 | .token.attr-value {
64 | color: #032f62;
65 | }
66 |
67 | .token.punctuation,
68 | .token.operator {
69 | color: #393A34; /* no highlight */
70 | }
71 |
72 | .token.entity,
73 | .token.url,
74 | .token.symbol,
75 | .token.number,
76 | .token.boolean,
77 | .token.variable,
78 | .token.constant,
79 | .token.property,
80 | .token.regex,
81 | .token.inserted {
82 | color: #36acaa;
83 | }
84 |
85 | .token.keyword {
86 | color: #d73a49;
87 | }
88 |
89 | .token.atrule,
90 | .token.attr-name,
91 | .language-autohotkey .token.selector {
92 | color: #005cc5;
93 | }
94 |
95 | .token.function,
96 | .token.deleted,
97 | .language-autohotkey .token.tag {
98 | color: #6f42c1;
99 | }
100 |
101 | .token.tag,
102 | .token.selector,
103 | .language-autohotkey .token.keyword {
104 | color: #22863a;
105 | }
106 |
107 | .token.important,
108 | .token.bold {
109 | font-weight: bold;
110 | }
111 |
112 | .token.italic {
113 | font-style: italic;
114 | }
115 |
--------------------------------------------------------------------------------
/site/src/_data/compare.yaml:
--------------------------------------------------------------------------------
1 | rows:
2 | - title: Overall Viewer Experience
3 | field: viewer_experience_score
4 | description: >-
5 | Overall Viewer Experience is a high-level score from 0 to 100 that
6 | measures the QoE (Quality of Experience).
7 | - title: Startup Time Score
8 | field: startup_time_score
9 | description: >-
10 | Startup Time is the time between when the user attempts to start playback and when they see the first frame of video.
11 | - title: Video Startup Time
12 | field: video_startup_time
13 | description: >-
14 | Video Startup Time measures the time that the viewer waits for the video
15 | to play after the page is loaded and the player is ready.
16 | - title: Player Startup Time
17 | field: player_startup_time
18 | description: >-
19 | Player Startup Time measures the time from when the player is first
20 | initialized in the page to when it is ready to receive further
21 | instructions.
22 | - title: Smoothness Score
23 | field: smoothness_score
24 | description: >-
25 | Smoothness Score measures the amount of rebuffering a viewer sees when
26 | watching video. A higher Smoothness Score means the viewer experiences
27 | less rebuffering, while a lower score means a viewer sees more
28 | rebuffering.
29 | - title: Rebuffer Percentage
30 | field: rebuffer_percentage
31 | description: >-
32 | Rebuffer Percentage measures the volume of rebuffering that is occurring across the platform.
33 | - title: Video Quality
34 | field: video_quality_score
35 | description: >-
36 | Video Quality compares the resolution of the video stream to the dimensions of the player.
37 | - title: Lighthouse Score
38 | field: category_performance_median
39 | description: >-
40 | The Lighthouse Performance score is a weighted average of the metric scores. Naturally, more heavily weighted metrics have a bigger effect on your overall Performance score. The metric scores are not visible in the report, but are calculated under the hood.
41 | players:
42 | apivideo: true
43 | vimeo: true
44 | youtube: true
45 | dailymotion: true
46 | brightcove: true
47 | facebook: true
48 | streamable: true
49 | wistia: true
50 | jwplayer: true
51 | vidyard: true
52 | muxvideo: true
53 | cloudflare: true
54 | cloudinary: true
55 |
--------------------------------------------------------------------------------
/site/src/css/prism-dracula.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Dracula Theme originally by Zeno Rocha [@zenorocha]
3 | * https://draculatheme.com/
4 | *
5 | * Ported for PrismJS by Albert Vallverdu [@byverdu]
6 | */
7 |
8 | .highlight-dark {
9 | code[class*="language-"],
10 | pre[class*="language-"] {
11 | color: #f8f8f2;
12 | @apply font-mono;
13 |
14 | direction: ltr;
15 | text-align: left;
16 | white-space: pre;
17 | word-spacing: normal;
18 | word-break: normal;
19 | line-height: 1.5;
20 |
21 | -moz-tab-size: 4;
22 | -o-tab-size: 4;
23 | tab-size: 4;
24 |
25 | -webkit-hyphens: none;
26 | -moz-hyphens: none;
27 | -ms-hyphens: none;
28 | hyphens: none;
29 | }
30 |
31 | /* Code blocks */
32 | pre[class*="language-"] {
33 | padding: 1em;
34 | margin: .5em 0;
35 | overflow: auto;
36 | border-radius: 0.3em;
37 | }
38 |
39 | :not(pre) > code[class*="language-"],
40 | pre[class*="language-"] {
41 | background: #282a36;
42 | }
43 |
44 | /* Inline code */
45 | :not(pre) > code[class*="language-"] {
46 | padding: .1em;
47 | border-radius: .3em;
48 | white-space: normal;
49 | }
50 |
51 | .token.comment,
52 | .token.prolog,
53 | .token.doctype,
54 | .token.cdata {
55 | color: #6272a4;
56 | }
57 |
58 | .token.punctuation {
59 | color: #f8f8f2;
60 | }
61 |
62 | .namespace {
63 | opacity: .7;
64 | }
65 |
66 | .token.property,
67 | .token.tag,
68 | .token.constant,
69 | .token.symbol,
70 | .token.deleted {
71 | color: #ff79c6;
72 | }
73 |
74 | .token.boolean,
75 | .token.number {
76 | color: #bd93f9;
77 | }
78 |
79 | .token.selector,
80 | .token.attr-name,
81 | .token.string,
82 | .token.char,
83 | .token.builtin,
84 | .token.inserted {
85 | color: #50fa7b;
86 | }
87 |
88 | .token.operator,
89 | .token.entity,
90 | .token.url,
91 | .language-css .token.string,
92 | .style .token.string,
93 | .token.variable {
94 | color: #f8f8f2;
95 | }
96 |
97 | .token.atrule,
98 | .token.attr-value,
99 | .token.function,
100 | .token.class-name {
101 | color: #f1fa8c;
102 | }
103 |
104 | .token.keyword {
105 | color: #8be9fd;
106 | }
107 |
108 | .token.regex,
109 | .token.important {
110 | color: #ffb86c;
111 | }
112 |
113 | .token.important,
114 | .token.bold {
115 | font-weight: bold;
116 | }
117 |
118 | .token.italic {
119 | font-style: italic;
120 | }
121 |
122 | .token.entity {
123 | cursor: help;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/site/src/_data/lighthouse.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 | const yaml = require('js-yaml');
3 | const fs = require('fs');
4 | // const playerx = require('playerx/dist/config.umd.js');
5 |
6 | require('dotenv').config();
7 |
8 | const projectId = '49de72e2-f68e-40c4-be22-26ab2a2917b0';
9 | const apiUrl = `https://lhci.playerx.io/v1/projects/${projectId}`;
10 |
11 | module.exports = async () => {
12 | // const buildId = (
13 | // await axios.get(`${apiUrl}/builds?limit=1&lifecycle=sealed`, {
14 | // timeout: 1000,
15 | // })
16 | // ).data[0]?.id;
17 |
18 | // if (buildId) {
19 | // await Promise.all([
20 | // fetchMetric(
21 | // 'category_performance_median',
22 | // `${apiUrl}/builds/${buildId}/statistics`
23 | // ),
24 | // ]);
25 | // }
26 |
27 | return yaml.load(fs.readFileSync(`${__dirname}/players.yaml`, 'utf8'));
28 | };
29 |
30 | function fetchMetric(name, url) {
31 | return axios
32 | .get(url, {
33 | timeout: 1000,
34 | })
35 | .then((response) => {
36 | let players;
37 | let input;
38 | try {
39 | input = fs.readFileSync(`${__dirname}/players.yaml`, 'utf8');
40 | players = yaml.load(input);
41 | } catch (e) {
42 | console.log(e);
43 | }
44 |
45 | let metrics = response.data;
46 | metrics = metrics.filter((item) => item.name === name);
47 | metrics = metrics.map((item) => {
48 | // Get the url param from `https://api.playerx.io/render?url=` if needed.
49 | let renderUrl = new URL(item.url);
50 | return {
51 | ...item,
52 | testUrl: renderUrl.searchParams.get('url') || item.url,
53 | };
54 | });
55 |
56 | for (let player of players) {
57 | if (!playerx[player.key]) continue;
58 |
59 | // There is a bug where hls.js is given a LH performance score of Mux
60 | // ignore for now as the standalone players are not on the compare page.
61 | const metric = metrics.find((item) => {
62 | return new RegExp(playerx[player.key].srcPattern).test(item.testUrl);
63 | });
64 | if (metric) {
65 | player[name] = metric.value;
66 | player.lighthouse_test_url = metric.url;
67 | }
68 | }
69 |
70 | if (input !== yaml.dump(players, { lineWidth: -1 })) {
71 | fs.writeFileSync(
72 | `${__dirname}/players.yaml`,
73 | yaml.dump(players, { lineWidth: -1 })
74 | );
75 | } else {
76 | console.log('No changes in players.yaml');
77 | }
78 | })
79 | .catch((err) => {
80 | console.log(err);
81 | });
82 | }
83 |
--------------------------------------------------------------------------------
/src/config/helpers.js:
--------------------------------------------------------------------------------
1 | const defaults = {
2 | npmCdn: 'https://cdn.jsdelivr.net/npm',
3 | callback: null,
4 | controls: true,
5 | params: '',
6 | options: `{}`,
7 |
8 | videoAttrs: `{{class=}}{{id=}}{{width=}}{{height=}}{{src=}}{{poster=}}{{preload=}}{{autoplay?}}{{muted?}}{{loop?}}{{controls?}}{{playsinline?}}{{autopictureinpicture?}}{{controlslist=}}{{crossorigin=}}`,
9 |
10 | video: '',
11 |
12 | allow: `accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture`,
13 |
14 | iframe: ``,
15 |
16 | node: `document.querySelector('#{{id}}')`,
17 | css: '',
18 | js: '',
19 | script: '',
20 | setup: `
21 | {{html}}
22 | {{js}}
23 | {{script}}
24 | `,
25 | };
26 |
27 | export function getHtml(opts) {
28 | let matches = [];
29 | if (opts.src && opts.srcPattern) {
30 | matches = opts.src.match(opts.srcPattern);
31 | }
32 |
33 | opts = {
34 | ...defaults,
35 | ...matches,
36 | metaId: matches[1],
37 | id: 'plx' + tinySimpleHash(opts.src),
38 | ...opts,
39 | };
40 |
41 | // Poor man's recursion
42 | render(opts);
43 | render(opts);
44 |
45 | if (opts.type === 'inline' || [true, '1', 'true'].includes(opts.api)) {
46 | return opts.setup;
47 | }
48 | return opts.html;
49 | }
50 |
51 | function render(opts) {
52 | Object.keys(opts).forEach((key) => {
53 | const opt = opts[key];
54 | if (typeof opt === 'string') {
55 | opts[key] = populate(opt, opts);
56 | }
57 | });
58 | }
59 |
60 | export function populate(template, obj) {
61 | return template.replace(
62 | /\{\{\s*([\w-]+)([=?|])?([^\s}]+?)?\s*\}\}/g,
63 | function (match, key, mod, fallback) {
64 | let val = obj[key];
65 | val = val != null ? val : fallback;
66 | if (val != null) {
67 | // mod for adding html value attributes
68 | if (mod === '=') {
69 | return ` ${key}="${val}"`;
70 | }
71 | // mod for adding html boolean attributes
72 | if (mod === '?') {
73 | if ([true, '1', 'true'].includes(val)) return ` ${key}`;
74 | return '';
75 | }
76 | return val;
77 | }
78 | return '';
79 | }
80 | );
81 | }
82 |
83 | /**
84 | * Create a truncated hash based on an input string.
85 | * So the returned id will be the same for a specific video src url.
86 | * https://stackoverflow.com/a/52171480/268820
87 | */
88 | function tinySimpleHash(s, len = 3) {
89 | for (var i = 0, h = 9; i < s.length; )
90 | h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
91 | return String(h ^ (h >>> 9)).slice(-len);
92 | }
93 |
--------------------------------------------------------------------------------
/site/src/_data/players.yaml:
--------------------------------------------------------------------------------
1 | - key: muxplayer
2 | name: Mux
3 | url: https://mux.com
4 | favicon: /images/mux-logo.png
5 | clips:
6 | - https://stream.mux.com/r4rOE02cc95tbe3I00302nlrHfT023Q3IedFJW029w018KxZA.m3u8?player=muxplayer
7 | - https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8?player=muxplayer
8 | - https://stream.mux.com/KHdaCPpki1o4PUCMWfAXFbrHy2gBosxdxZISdvFJGLQ.m3u8?player=muxplayer
9 | - key: vimeo
10 | name: Vimeo
11 | color: '#00ADEF'
12 | url: https://vimeo.com
13 | favicon: https://vimeo.com/favicon.ico
14 | clips:
15 | - https://vimeo.com/648359100
16 | - https://vimeo.com/638371189
17 | - https://vimeo.com/638371504
18 | - key: youtube
19 | name: YouTube
20 | color: '#FF0000'
21 | url: https://www.youtube.com
22 | favicon: https://www.youtube.com/s/desktop/e109ce07/img/favicon.ico
23 | clips:
24 | - https://www.youtube.com/watch?v=uxsOYVWclA0
25 | - https://www.youtube.com/watch?v=ssdng3QeFnA
26 | - https://www.youtube.com/watch?v=zD6nk5byNGE
27 | - key: wistia
28 | name: Wistia
29 | color: '#1E64F0'
30 | url: https://wistia.com
31 | favicon: https://wistia.com/static/favicon.ico
32 | clips:
33 | - https://wesleyluyten.wistia.com/medias/oifkgmxnkb
34 | - https://wesleyluyten.wistia.com/medias/oj8d7cwhbn
35 | - https://wesleyluyten.wistia.com/medias/1ekn652fs5
36 | - key: jwplayer
37 | name: JW Player
38 | color: '#FF0046'
39 | url: https://www.jwplayer.com
40 | favicon: https://www.jwplayer.com/hubfs/JW_Player_August2021/Images/favicon-152.png
41 | clips:
42 | - https://cdn.jwplayer.com/players/C8YE48zj-IxzuqJ4M.html
43 | - https://cdn.jwplayer.com/players/hAETCxXu-Pd4r8gwe.html
44 | - https://cdn.jwplayer.com/players/R12Nj7bO-Pd4r8gwe.html
45 | options:
46 | player: IxzuqJ4M
47 | - key: dash
48 | name: dash.js
49 | url: https://github.com/Dash-Industry-Forum/dash.js
50 | clips:
51 | - https://player.vimeo.com/external/648359100.mpd?s=a4419a2e2113cc24a87aef2f93ef69a8e4c8fb0c&ext=.mpd
52 | - https://dash.akamaized.net/envivio/EnvivioDash3/manifest.mpd
53 | - https://amssamples.streaming.mediaservices.windows.net/634cd01c-6822-4630-8444-8dd6279f94c6/CaminandesLlamaDrama4K.ism/manifest(format=mpd-time-csf)?ext=.mpd
54 | - key: hls
55 | name: hls.js
56 | url: https://github.com/video-dev/hls.js
57 | clips:
58 | - https://stream.mux.com/r4rOE02cc95tbe3I00302nlrHfT023Q3IedFJW029w018KxZA.m3u8
59 | - https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8
60 | - https://stream.mux.com/KHdaCPpki1o4PUCMWfAXFbrHy2gBosxdxZISdvFJGLQ.m3u8
61 | - key: html
62 | name: HTML
63 | url: https://www.w3.org/2010/05/video/mediaevents.html
64 | clips:
65 | - https://stream.mux.com/r4rOE02cc95tbe3I00302nlrHfT023Q3IedFJW029w018KxZA/high.mp4?player=html
66 | - https://ia600300.us.archive.org/17/items/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4
67 |
--------------------------------------------------------------------------------
/site/src/pages/matrix.njk:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: matrix/
3 | title: "Playerx - Video Matrix"
4 | menu: Matrix
5 | eleventyNavigation:
6 | key: Matrix
7 | order: 4
8 | tags:
9 | - main
10 | ---
11 |
12 | {% extends "layouts/base.njk" %}
13 |
14 | {% block head %}
15 | {{ super() }}
16 |
17 |
18 | {% endblock %}
19 |
20 | {% block content %}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {%- for player in players | sort(true, false, 'viewer_experience_score') -%}
51 | {% if matrix.players[player.key] %}
52 |
66 | {% endif %}
67 | {%- endfor -%}
68 |
69 |
70 |
71 | {% endblock %}
72 |
--------------------------------------------------------------------------------
/site/src/_data/muxify.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 | const yaml = require('js-yaml');
3 | const fs = require('fs');
4 |
5 | require('dotenv').config();
6 |
7 | const token = Buffer.from(`${process.env.MUX_TOKEN_ID}:${process.env.MUX_TOKEN_SECRET}`, 'utf8').toString('base64');
8 |
9 | module.exports = async () => {
10 |
11 | // await Promise.all([
12 | // fetchMetric('viewer_experience_score', 'https://api.mux.com/data/v1/metrics/viewer_experience_score/breakdown?order_by=value&order_direction=desc&measurement=avg&timeframe%5B%5D=1%3Adays&group_by=player_software&limit=50&page=1'),
13 |
14 | // fetchMetric('video_startup_time', 'https://api.mux.com/data/v1/metrics/video_startup_time/breakdown?order_by=value&order_direction=asc&measurement=median&timeframe%5B%5D=1%3Adays&group_by=player_software&limit=50&page=1'),
15 |
16 | // fetchMetric('player_startup_time', 'https://api.mux.com/data/v1/metrics/player_startup_time/breakdown?order_by=value&order_direction=asc&measurement=median&timeframe%5B%5D=1%3Adays&group_by=player_software&limit=50&page=1'),
17 |
18 | // fetchMetric('smoothness_score', 'https://api.mux.com/data/v1/metrics/smoothness_score/breakdown?order_by=value&order_direction=desc&measurement=avg&timeframe%5B%5D=1%3Adays&group_by=player_software&limit=50&page=1'),
19 |
20 | // fetchMetric('rebuffer_percentage', 'https://api.mux.com/data/v1/metrics/rebuffer_percentage/breakdown?order_by=value&order_direction=asc&measurement=avg&timeframe%5B%5D=1%3Adays&group_by=player_software&limit=50&page=1'),
21 |
22 | // fetchMetric('video_quality_score', 'https://api.mux.com/data/v1/metrics/video_quality_score/breakdown?order_by=value&order_direction=asc&measurement=avg&timeframe%5B%5D=1%3Adays&group_by=player_software&limit=50&page=1'),
23 |
24 | // fetchMetric('startup_time_score', 'https://api.mux.com/data/v1/metrics/startup_time_score/breakdown?order_by=value&order_direction=asc&measurement=avg&timeframe%5B%5D=1%3Adays&group_by=player_software&limit=50&page=1')
25 | // ]);
26 |
27 | return yaml.load(fs.readFileSync(`${__dirname}/players.yaml`, 'utf8'));
28 | };
29 |
30 | function fetchMetric(name, url) {
31 | return axios.get(url, {
32 | timeout: 1000,
33 | headers: {
34 | 'Authorization': `Basic ${token}`
35 | }
36 | })
37 | .then(response => {
38 | let players;
39 | let input;
40 | try {
41 | input = fs.readFileSync(`${__dirname}/players.yaml`, 'utf8');
42 | players = yaml.load(input);
43 | } catch (e) {
44 | console.log(e);
45 | }
46 |
47 | const metrics = response.data.data;
48 | for (let player of players) {
49 | const metric = metrics.find((obj) => obj.field === player.key);
50 | if (metric) {
51 | player[name] = metric.value;
52 | }
53 | }
54 |
55 | players.sort((a, b) => b.viewer_experience_score - a.viewer_experience_score);
56 |
57 | if (input !== yaml.dump(players, { lineWidth: -1 })) {
58 | fs.writeFileSync(`${__dirname}/players.yaml`, yaml.dump(players, { lineWidth: -1 }));
59 | } else {
60 | console.log('No changes in players.yaml');
61 | }
62 |
63 | })
64 | .catch(err => {
65 | console.log(err);
66 | });
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/site/src/js/matrix.js:
--------------------------------------------------------------------------------
1 | import { observable, computed, subscribe } from 'sinuous/observable';
2 | import { dhtml, hydrate as hy } from 'sinuous/hydrate';
3 |
4 | const clip = observable(1);
5 | const playing = observable(false);
6 | const duration = observable(0);
7 | const currentTime = observable(0);
8 | const currentTimeValue = observable(0);
9 |
10 | let src = function () {
11 | const { el } = this;
12 | return computed(() => {
13 | return el.dataset[`clip${clip()}`];
14 | });
15 | };
16 |
17 | let clipClass = function () {
18 | const { el } = this;
19 | return computed(() => {
20 | const active = clip()+'' === el.dataset.clip ? 'btn-active' : '';
21 | return `btn btn-clip${el.dataset.clip} ${active}`;
22 | });
23 | };
24 |
25 | hy(dhtml`
26 |
27 |
34 | `);
35 |
36 | hy(dhtml`
37 | currentTimeValue() / duration() || 0}
39 | oninput=${(e) => currentTime(e.target.value * duration())} />
40 | `);
41 |
42 | const props = {
43 | src,
44 | currentTime,
45 | ondurationchange: () => player.duration && duration(player.duration),
46 | onseeking: () => currentTimeValue(player.currentTime),
47 | onseeked: () => currentTimeValue(player.currentTime),
48 | ontimeupdate: throttle(() => {
49 | currentTimeValue(player.currentTime);
50 | }, 500),
51 | style: '--controls: none;'
52 | };
53 |
54 | let gridColsClass = observable('grid-cols-4');
55 | let players = hy(dhtml`
56 |
57 | ${[...Array(30)].map(
58 | () => dhtml`
59 |
62 | `
63 | )}
64 |
65 | `);
66 |
67 | let player = players.querySelector('player-x');
68 |
69 | subscribe(() => {
70 | const isPaused = !playing();
71 | for (let p of players.querySelectorAll('player-x')) {
72 | if (p.paused !== isPaused) {
73 | !isPaused ? p.play() : p.pause();
74 | }
75 | }
76 | });
77 |
78 | let underlineClass = function () {
79 | const { el } = this;
80 | return computed(() => {
81 | const underline = gridColsClass() === `grid-cols-${el.textContent}`;
82 | return `grid-btn ${underline ? 'underline' : ''}`;
83 | });
84 | };
85 |
86 | hy(dhtml`
87 |
88 | ${[...Array(4)].map(
89 | () => dhtml` {
90 | gridColsClass(`grid-cols-${e.currentTarget.textContent}`);
91 | }} />`
92 | )}
93 |
94 | `);
95 |
96 |
97 | function throttle(func, timeFrame) {
98 | var lastTime = 0;
99 | return function () {
100 | var now = new Date();
101 | if (now - lastTime >= timeFrame) {
102 | func();
103 | lastTime = now;
104 | }
105 | };
106 | }
107 |
--------------------------------------------------------------------------------
/site/src/images/symbol-defs.svg:
--------------------------------------------------------------------------------
1 |
25 |
--------------------------------------------------------------------------------
/site/src/js/site.js:
--------------------------------------------------------------------------------
1 | /* global galite */
2 | import { observable } from 'sinuous';
3 | import { hydrate as hy, dhtml } from 'sinuous/hydrate';
4 | import { dropdown } from './helpers/dropdown.js';
5 | import { cx } from './utils/dom.js';
6 | import { invert } from './utils/utils.js';
7 |
8 | const burgerIsActive = observable(false);
9 | const openBurgerMenu = invert(burgerIsActive);
10 | const docsDropdown = dropdown();
11 |
12 | hy(dhtml`
15 | `);
16 |
17 | hy(dhtml`
19 | `);
20 |
21 | hy(dhtml`
22 | docsDropdown.close()} />
23 | `);
24 |
25 | // hy(dhtml`
26 | //
33 | // `);
34 |
35 | hy(dhtml``);
36 |
37 | function openTweetWindow(e) {
38 | e.preventDefault();
39 | window.open(
40 | e.target.href,
41 | 'Twitter',
42 | `top=${screen.height / 2 - 285},left=${
43 | screen.width / 2 - 550 / 2
44 | },width=550,height=285,toolbar=no,location=0,status=no,menubar=no,scrollbars=yes,resizable=1`
45 | );
46 | }
47 |
48 | function prefetch(e) {
49 | if (e.target.tagName != 'A') {
50 | return;
51 | }
52 | if (e.target.origin != location.origin) {
53 | return;
54 | }
55 | var l = document.createElement('link');
56 | l.rel = 'prefetch';
57 | l.href = e.target.href;
58 | document.head.appendChild(l);
59 | }
60 | document.documentElement.addEventListener('mouseover', prefetch, {
61 | capture: true,
62 | passive: true,
63 | });
64 | document.documentElement.addEventListener('touchstart', prefetch, {
65 | capture: true,
66 | passive: true,
67 | });
68 |
69 | const GA_ID = document.documentElement.getAttribute('ga-id');
70 |
71 | galite.l = +new Date();
72 | galite('create', GA_ID, 'auto');
73 | galite('set', 'transport', 'beacon');
74 | var timeout = setTimeout(
75 | (onload = function () {
76 | clearTimeout(timeout);
77 | galite('send', 'pageview');
78 | }),
79 | 1000
80 | );
81 |
82 | var ref = +new Date();
83 | function ping(event) {
84 | var now = +new Date();
85 | if (now - ref < 1000) {
86 | return;
87 | }
88 | galite('send', {
89 | hitType: 'event',
90 | eventCategory: 'page',
91 | eventAction: event.type,
92 | eventLabel: Math.round((now - ref) / 1000),
93 | });
94 | ref = now;
95 | }
96 | addEventListener('pagehide', ping);
97 | addEventListener('visibilitychange', ping);
98 | addEventListener(
99 | 'click',
100 | function (e) {
101 | var button = e.target.closest('button');
102 | if (!button) {
103 | return;
104 | }
105 | galite('send', {
106 | hitType: 'event',
107 | eventCategory: 'button',
108 | eventAction: button.getAttribute('aria-label') || button.textContent,
109 | });
110 | },
111 | true
112 | );
113 | var selectionTimeout;
114 | addEventListener(
115 | 'selectionchange',
116 | function () {
117 | clearTimeout(selectionTimeout);
118 | var text = String(document.getSelection()).trim();
119 | if (text.split(/[\s\n\r]+/).length < 3) {
120 | return;
121 | }
122 | selectionTimeout = setTimeout(function () {
123 | galite('send', {
124 | hitType: 'event',
125 | eventCategory: 'selection',
126 | eventAction: text,
127 | });
128 | }, 2000);
129 | },
130 | true
131 | );
132 |
--------------------------------------------------------------------------------
/site/.eleventy.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const yaml = require('js-yaml');
3 | const { DateTime } = require('luxon');
4 | const pluginRss = require("@11ty/eleventy-plugin-rss");
5 | const eleventyNavigationPlugin = require('@11ty/eleventy-navigation');
6 | const syntaxHighlight = require('@11ty/eleventy-plugin-syntaxhighlight');
7 |
8 | module.exports = function (eleventyConfig) {
9 | // A useful way to reference the context we are runing eleventy in
10 | let env = process.env.NODE_ENV;
11 | // make the seed target act like prod
12 | env = env == 'seed' ? 'prod' : env;
13 |
14 | eleventyConfig.setServerOptions({
15 | enabled: true,
16 | // https: true,
17 | // port: 443,
18 | port: 80,
19 | });
20 |
21 | eleventyConfig.addPlugin(pluginRss);
22 | eleventyConfig.addPlugin(eleventyNavigationPlugin);
23 | eleventyConfig.addPlugin(syntaxHighlight, {
24 | // Change which syntax highlighters are installed
25 | templateFormats: ['*'], // default
26 |
27 | // Or, just njk and md syntax highlighters (do not install liquid)
28 | templateFormats: ['njk', 'md'],
29 |
30 | // init callback lets you customize Prism
31 | init: function () {
32 | // prismTemplates(Prism);
33 | },
34 | });
35 |
36 | eleventyConfig.addDataExtension('yaml', (contents) =>
37 | yaml.load(contents)
38 | );
39 |
40 | // Layout aliases can make templates more portable
41 | eleventyConfig.addLayoutAlias('default', 'layouts/base.njk');
42 |
43 | // Add some utility filters
44 | eleventyConfig.addFilter("readableDate", dateObj => {
45 | return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat("dd LLL yyyy");
46 | });
47 |
48 | // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string
49 | eleventyConfig.addFilter('htmlDateString', (dateObj) => {
50 | return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat('yyyy-LL-dd');
51 | });
52 |
53 | eleventyConfig.addFilter('squash', require('./src/utils/filters/squash.js'));
54 | eleventyConfig.addFilter(
55 | 'dateDisplay',
56 | require('./src/utils/filters/date.js')
57 | );
58 |
59 | // minify the html output
60 | eleventyConfig.addTransform('htmlmin', require('./src/utils/minify-html.js'));
61 |
62 | eleventyConfig.addShortcode('getCdnUrl', function (name) {
63 | let prefix = '';
64 | if (env === 'prod') {
65 | const deps = require('./package.json').dependencies;
66 | const stripSubmod = name.replace(/\/.*$/, '');
67 | const version = deps.playerx || 'latest';
68 |
69 | const [pkg, mod] = name.split('/');
70 | return `https://cdn.jsdelivr.net/npm/playerx@${version}/+esm`;
71 |
72 | } else {
73 | const [pkg, mod] = name.split('/');
74 | return `http://localhost:8000/${mod || pkg}.js`;
75 | }
76 | });
77 |
78 | // compress and combine js files
79 | eleventyConfig.addFilter('jsmin', function (code) {
80 | if (env === 'prod') {
81 | const Terser = require('terser');
82 | let minified = Terser.minify(code);
83 | if (minified.error) {
84 | console.log('Terser error: ', minified.error);
85 | return code;
86 | }
87 | return minified.code;
88 | }
89 | return code;
90 | });
91 |
92 | // pass some assets right through
93 | eleventyConfig.addPassthroughCopy('./src/images');
94 | eleventyConfig.addPassthroughCopy('./src/fonts');
95 | eleventyConfig.addPassthroughCopy('src/favicon.ico');
96 | eleventyConfig.addPassthroughCopy('./src/js/ga.js');
97 |
98 | return {
99 | dir: {
100 | input: 'src',
101 | output: 'public',
102 | data: `_data/${env}`,
103 | },
104 | templateFormats: ['html', 'njk', 'md', '11ty.js'],
105 | htmlTemplateEngine: 'njk',
106 | markdownTemplateEngine: 'njk',
107 | passthroughFileCopy: true,
108 | };
109 | };
110 |
--------------------------------------------------------------------------------
/src/playerx.js:
--------------------------------------------------------------------------------
1 | import { SuperVideoElement } from 'super-media-element';
2 | import { options } from './options.js';
3 |
4 | const template = globalThis.document?.createElement('template');
5 | if (template) {
6 | template.innerHTML = `
7 | ${SuperVideoElement.template.innerHTML}
8 |
14 | `;
15 | }
16 |
17 | class Playerx extends SuperVideoElement {
18 | static template = template;
19 |
20 | attributeChangedCallback(attrName, oldValue, newValue) {
21 | if (newValue == oldValue) return;
22 | super.attributeChangedCallback(attrName, oldValue, newValue);
23 | }
24 |
25 | async load() {
26 | const canLoadSource = canPlayerLoadSource(this);
27 | if (!canLoadSource) {
28 | this.nativeEl?.remove();
29 | this.nativeEl = null;
30 | }
31 |
32 | if (!this.src) {
33 | return;
34 | }
35 |
36 | this.key = getCurrentPlayerConfigKey(this.src);
37 | const config = options.players[this.key];
38 | config.npmCdn = options.npmCdn;
39 |
40 | const jsUrl = config.jsUrl && populate(config.jsUrl, config);
41 | if (jsUrl) {
42 | await loadScript(jsUrl);
43 | }
44 |
45 | if (!canLoadSource && !this.querySelector(config.type)) {
46 | this.nativeEl = this.appendChild(document.createElement(config.type));
47 | }
48 |
49 | this.nativeEl.toggleAttribute('autoplay', this.autoplay);
50 | this.nativeEl.toggleAttribute('loop', this.loop);
51 | this.nativeEl.toggleAttribute('controls', this.controls);
52 | this.nativeEl.toggleAttribute('muted', this.defaultMuted || this.muted);
53 | this.nativeEl.toggleAttribute('playsinline', this.playsInline);
54 | }
55 |
56 | get api() {
57 | return this.nativeEl;
58 | }
59 | }
60 |
61 | function canPlayerLoadSource(element) {
62 | const playerParam = getSrcParam(element.src, 'player');
63 | if (playerParam && playerParam !== element.key) {
64 | return false;
65 | }
66 | return (
67 | element.api && options.players[element.key].pattern?.test(element.src)
68 | );
69 | }
70 |
71 | function getCurrentPlayerConfigKey(src) {
72 | const playerParam = getSrcParam(src, 'player');
73 | if (options.players[playerParam]) {
74 | return playerParam;
75 | }
76 |
77 | for (let key in options.players) {
78 | const playerConfig = options.players[key];
79 | if (playerConfig.pattern?.test(src)) {
80 | return key;
81 | }
82 | }
83 | // Fallback to html player.
84 | return 'html';
85 | }
86 |
87 | function getSrcParam(src, key) {
88 | const url = Array.isArray(src) ? src[0] : src;
89 | return url && new URLSearchParams(url.split('?')[1]).get(key);
90 | }
91 |
92 | function populate(tpl, obj) {
93 | return tpl.replace(
94 | /\{\{\s*([\w-]+)([=?|])?([^\s}]+?)?\s*\}\}/g,
95 | function (match, key, mod, fallback) {
96 | let val = obj[key];
97 | val = val != null ? val : fallback;
98 | if (val != null) {
99 | return val;
100 | }
101 | return '';
102 | }
103 | );
104 | }
105 |
106 | const loadScriptCache = {};
107 | async function loadScript(src, globalName) {
108 | if (!globalName) return import(src);
109 | if (loadScriptCache[src]) return loadScriptCache[src];
110 | if (self[globalName]) return self[globalName];
111 | return (loadScriptCache[src] = new Promise((resolve, reject) => {
112 | const script = document.createElement('script');
113 | script.defer = true;
114 | script.src = src;
115 | script.onload = () => resolve(self[globalName]);
116 | script.onerror = reject;
117 | document.head.append(script);
118 | }));
119 | }
120 |
121 | if (globalThis.customElements && !globalThis.customElements.get('player-x')) {
122 | globalThis.customElements.define('player-x', Playerx);
123 | }
124 |
125 | export { options, Playerx };
126 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
2 |
3 | [](https://www.npmjs.com/package/playerx)
4 | [](https://www.npmjs.com/package/playerx)
5 | [](https://www.jsdelivr.com/package/npm/playerx)
6 | [](https://bundlephobia.com/result?p=playerx)
7 |
8 | **npm**: `npm i playerx`
9 | **cdn**: https://cdn.jsdelivr.net/npm/playerx/+esm
10 |
11 | ## Features
12 |
13 | - 🏄♂️ Compatible [`HTMLMediaElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement) API
14 | - 🕺 Seamlessly integrates with [Media Chrome](https://github.com/muxinc/media-chrome)
15 |
16 |
17 | ## Usage ([Codesandbox](https://codesandbox.io/s/hello-playerx-22ku4))
18 |
19 | ```html
20 |
21 |
22 | ```
23 |
24 | Demo page: [dev.playerx.io/demo](https://dev.playerx.io/demo/)
25 |
26 |
27 | ## Supported media
28 |
29 | * Mux videos use the [Mux Player API](https://github.com/muxinc/elements/blob/main/packages/mux-player/REFERENCE.md)
30 | * YouTube videos use the [YouTube iFrame Player API](https://developers.google.com/youtube/iframe_api_reference)
31 | * Vimeo videos use the [Vimeo Player API](https://developer.vimeo.com/player/sdk)
32 | * Wistia videos use the [Wistia Player API](https://wistia.com/doc/player-api)
33 | * JW Player videos use the [JW Player API](https://developer.jwplayer.com/jwplayer/docs/jw8-javascript-api-reference)
34 | * [Supported file types](https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats) are playing using [`