├── 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 |
13 |

14 | This page is part of the posts section. 15 |

16 |
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 |
18 | 19 |
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 | 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 | 32 |
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 |
60 | 61 |
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`
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`