cloudflare-video-element
Advanced tools
Comparing version 1.0.3 to 1.1.0
@@ -43,23 +43,59 @@ // https://developers.cloudflare.com/stream/viewing-videos/using-the-stream-player/using-the-player-api/ | ||
const templateShadowDOM = globalThis.document?.createElement('template'); | ||
templateShadowDOM.innerHTML = /*html*/` | ||
<style> | ||
:host { | ||
display: inline-block; | ||
min-width: 300px; | ||
min-height: 150px; | ||
position: relative; | ||
} | ||
iframe { | ||
position: absolute; | ||
top: 0; | ||
left: 0; | ||
} | ||
:host(:not([controls])) { | ||
pointer-events: none; | ||
} | ||
</style> | ||
`; | ||
function getTemplateHTML(attrs) { | ||
const iframeAttrs = { | ||
src: serializeIframeUrl(attrs), | ||
frameborder: 0, | ||
width: '100%', | ||
height: '100%', | ||
allow: 'accelerometer; fullscreen; autoplay; encrypted-media; gyroscope; picture-in-picture', | ||
}; | ||
return /*html*/` | ||
<style> | ||
:host { | ||
display: inline-block; | ||
min-width: 300px; | ||
min-height: 150px; | ||
position: relative; | ||
} | ||
iframe { | ||
position: absolute; | ||
top: 0; | ||
left: 0; | ||
} | ||
:host(:not([controls])) { | ||
pointer-events: none; | ||
} | ||
</style> | ||
<iframe${serializeAttributes(iframeAttrs)}></iframe> | ||
`; | ||
} | ||
function serializeIframeUrl(attrs) { | ||
if (!attrs.src) return; | ||
const matches = attrs.src.match(MATCH_SRC); | ||
const srcId = matches && matches[1]; | ||
const params = { | ||
// ?controls=true is enabled by default in the iframe | ||
controls: attrs.controls === '' ? null : 0, | ||
autoplay: attrs.autoplay, | ||
loop: attrs.loop, | ||
muted: attrs.muted, | ||
preload: attrs.preload, | ||
poster: attrs.poster, | ||
defaultTextTrack: attrs.defaulttexttrack, | ||
primaryColor: attrs.primarycolor, | ||
letterboxColor: attrs.letterboxcolor, | ||
startTime: attrs.starttime, | ||
'ad-url': attrs.adurl, | ||
}; | ||
return `${EMBED_BASE}/${srcId}?${serialize(params)}`; | ||
} | ||
class CloudflareVideoElement extends (globalThis.HTMLElement ?? class {}) { | ||
static getTemplateHTML = getTemplateHTML; | ||
static shadowRootOptions = { mode: 'open' }; | ||
static observedAttributes = [ | ||
@@ -77,23 +113,18 @@ 'autoplay', | ||
loadComplete = new PublicPromise(); | ||
#loadRequested; | ||
#hasLoaded; | ||
#noInit; | ||
#options; | ||
#isInit; | ||
#readyState = 0; | ||
constructor() { | ||
super(); | ||
async load() { | ||
if (this.#loadRequested) return; | ||
this.attachShadow({ mode: 'open' }); | ||
this.shadowRoot.append(templateShadowDOM.content.cloneNode(true)); | ||
if (this.#hasLoaded) this.loadComplete = new PublicPromise(); | ||
this.#hasLoaded = true; | ||
this.loadComplete = new PublicPromise(); | ||
} | ||
// Wait 1 tick to allow other attributes to be set. | ||
await (this.#loadRequested = Promise.resolve()); | ||
this.#loadRequested = null; | ||
async load() { | ||
if (this.#hasLoaded) { | ||
this.loadComplete = new PublicPromise(); | ||
this.#noInit = true; | ||
} | ||
this.#hasLoaded = true; | ||
this.#readyState = 0; | ||
@@ -105,5 +136,2 @@ this.dispatchEvent(new Event('emptied')); | ||
// Wait 1 tick to allow other attributes to be set. | ||
await Promise.resolve(); | ||
if (!this.src) { | ||
@@ -113,21 +141,6 @@ return; | ||
this.#options = { | ||
autoplay: this.autoplay, | ||
controls: this.controls, | ||
loop: this.loop, | ||
muted: this.defaultMuted, | ||
playsinline: this.playsInline, | ||
preload: this.preload, | ||
poster: this.poster, | ||
defaultTextTrack: this.getAttribute('defaulttexttrack'), | ||
primaryColor: this.getAttribute('primarycolor'), | ||
letterboxColor: this.getAttribute('letterboxcolor'), | ||
startTime: this.getAttribute('starttime'), | ||
'ad-url': this.getAttribute('adurl'), | ||
}; | ||
const matches = this.src.match(MATCH_SRC); | ||
const srcId = matches && matches[1]; | ||
if (this.#noInit) { | ||
if (this.#isInit) { | ||
@@ -138,16 +151,21 @@ this.api = oldApi; | ||
} else { | ||
this.#isInit = true; | ||
const src = `${EMBED_BASE}/${srcId}?${serialize(removeFalsy({ | ||
...this.#options, | ||
// ?controls=true is enabled by default in the iframe | ||
controls: this.#options.controls ? null : '0' | ||
}))}`; | ||
let serverRendered = this.shadowRoot; | ||
let iframe = this.shadowRoot.querySelector('iframe'); | ||
if (!iframe) { | ||
iframe = createEmbedIframe({ src }); | ||
this.shadowRoot.append(iframe); | ||
if (!this.shadowRoot) { | ||
this.attachShadow({ mode: 'open' }); | ||
this.shadowRoot.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes)); | ||
} | ||
const iframe = this.shadowRoot.querySelector('iframe'); | ||
const Stream = await loadScript(API_URL, API_GLOBAL); | ||
if (serverRendered) { | ||
// The Cloudflare Player API has a bug where it doesn't work with a SSR iframe | ||
// because it loads too quickly and the `iframeReady` post message is lost. | ||
// To work around this, we need to reload the iframe. | ||
iframe.src = `${iframe.src}`; | ||
} | ||
this.api = Stream(iframe); | ||
@@ -176,3 +194,3 @@ | ||
this.api.addEventListener('loadstart', () => { | ||
Promise.resolve().then(() => { | ||
this.dispatchEvent(new Event('loadcomplete')); | ||
@@ -186,8 +204,8 @@ this.loadComplete.resolve(); | ||
async attributeChangedCallback(attrName, oldValue, newValue) { | ||
if (oldValue === newValue) return; | ||
// This is required to come before the await for resolving loadComplete. | ||
switch (attrName) { | ||
case 'src': { | ||
if (oldValue !== newValue) { | ||
this.load(); | ||
} | ||
this.load(); | ||
return; | ||
@@ -203,5 +221,3 @@ } | ||
case 'loop': { | ||
if (this.#options[attrName] !== this.hasAttribute(attrName)) { | ||
this.api[attrName] = this.hasAttribute(attrName); | ||
} | ||
this.api[attrName] = this.hasAttribute(attrName); | ||
break; | ||
@@ -211,5 +227,3 @@ } | ||
case 'preload': { | ||
if (this.#options[attrName] !== this.getAttribute(attrName)) { | ||
this.api[attrName] = this.getAttribute(attrName); | ||
} | ||
this.api[attrName] = this.getAttribute(attrName); | ||
break; | ||
@@ -390,2 +404,35 @@ } | ||
function serializeAttributes(attrs) { | ||
let html = ''; | ||
for (const key in attrs) { | ||
const value = attrs[key]; | ||
if (value === '') html += ` ${key}`; | ||
else html += ` ${key}="${value}"`; | ||
} | ||
return html; | ||
} | ||
function serialize(props) { | ||
return String(new URLSearchParams(boolToBinary(props))); | ||
} | ||
function boolToBinary(props) { | ||
let p = {}; | ||
for (let key in props) { | ||
let val = props[key]; | ||
if (val === true || val === '') p[key] = 1; | ||
else if (val === false) p[key] = 0; | ||
else if (val != null) p[key] = val; | ||
} | ||
return p; | ||
} | ||
function namedNodeMapToObject(namedNodeMap) { | ||
let obj = {}; | ||
for (let attr of namedNodeMap) { | ||
obj[attr.name] = attr.value; | ||
} | ||
return obj; | ||
} | ||
const loadScriptCache = {}; | ||
@@ -426,33 +473,2 @@ async function loadScript(src, globalName) { | ||
function createElement(tag, attrs = {}, ...children) { | ||
const el = document.createElement(tag); | ||
Object.keys(attrs).forEach( | ||
(name) => attrs[name] != null && el.setAttribute(name, attrs[name]) | ||
); | ||
el.append(...children); | ||
return el; | ||
} | ||
const allow = | ||
'accelerometer; fullscreen; autoplay; encrypted-media; gyroscope; picture-in-picture'; | ||
function createEmbedIframe({ src, ...props }) { | ||
return createElement('iframe', { | ||
src, | ||
width: '100%', | ||
height: '100%', | ||
allow, | ||
frameborder: 0, | ||
...props, | ||
}); | ||
} | ||
function serialize(props) { | ||
return String(new URLSearchParams(props)); | ||
} | ||
function removeFalsy(obj) { | ||
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v)); | ||
} | ||
if (globalThis.customElements && !globalThis.customElements.get('cloudflare-video')) { | ||
@@ -459,0 +475,0 @@ globalThis.customElements.define('cloudflare-video', CloudflareVideoElement); |
{ | ||
"name": "cloudflare-video-element", | ||
"version": "1.0.3", | ||
"version": "1.1.0", | ||
"description": "A custom element for the Cloudflare player with an API that matches the `<video>` API", | ||
"type": "module", | ||
"main": "cloudflare-video-element.js", | ||
"repository": "luwes/cloudflare-video-element", | ||
"author": "Wesley Luyten <me@wesleyluyten.com> (https://wesleyluyten.com)", | ||
"author": "@muxinc", | ||
"license": "MIT", | ||
"homepage": "https://github.com/luwes/cloudflare-video-element#readme", | ||
"homepage": "https://github.com/muxinc/media-elements#readme", | ||
"bugs": { | ||
"url": "https://github.com/luwes/cloudflare-video-element/issues" | ||
"url": "https://github.com/muxinc/media-elements/issues" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/muxinc/media-elements.git", | ||
"directory": "packages/cloudflare-video-element" | ||
}, | ||
"files": [ | ||
"dist" | ||
], | ||
"type": "module", | ||
"main": "cloudflare-video-element.js", | ||
"exports": { | ||
".": "./cloudflare-video-element.js", | ||
"./react": "./dist/react.js" | ||
}, | ||
"scripts": { | ||
"lint": "npx eslint@8 *.js -c ./node_modules/wet-run/.eslintrc.json", | ||
"lint": "npx eslint@8 *.js", | ||
"test": "wet run", | ||
"dev": "wet serve" | ||
"serve": "wet serve", | ||
"build:react": "build-react", | ||
"build": "run-s build:*" | ||
}, | ||
"devDependencies": { | ||
"npm-run-all": "^4.1.5", | ||
"wet-run": "^1.2.2" | ||
@@ -21,0 +35,0 @@ }, |
# `<cloudflare-video>` | ||
[![Version](https://img.shields.io/npm/v/cloudflare-video-element?style=flat-square&color=success)](https://www.npmjs.com/package/cloudflare-video-element) | ||
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/cloudflare-video-element?style=flat-square&label=gzip)](https://bundlephobia.com/result?p=cloudflare-video-element) | ||
[![NPM Version](https://img.shields.io/npm/v/cloudflare-video-element?style=flat-square&color=informational)](https://www.npmjs.com/package/cloudflare-video-element) | ||
[![NPM Downloads](https://img.shields.io/npm/dm/cloudflare-video-element?style=flat-square&color=informational&label=npm)](https://www.npmjs.com/package/cloudflare-video-element) | ||
[![jsDelivr hits (npm)](https://img.shields.io/jsdelivr/npm/hm/cloudflare-video-element?style=flat-square&color=%23FF5627)](https://www.jsdelivr.com/package/npm/cloudflare-video-element) | ||
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/cloudflare-video-element?style=flat-square&color=success&label=gzip)](https://bundlephobia.com/result?p=cloudflare-video-element) | ||
@@ -6,0 +8,0 @@ A [custom element](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
60
0
14607
2
3
383
1