Skip to content

Commit

Permalink
feat: add SSR to vimeo-video-element
Browse files Browse the repository at this point in the history
  • Loading branch information
luwes committed Apr 29, 2024
1 parent 8a532f2 commit c44e755
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 124 deletions.
28 changes: 13 additions & 15 deletions packages/cloudflare-video-element/cloudflare-video-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,44 +109,42 @@ class CloudflareVideoElement extends (globalThis.HTMLElement ?? class {}) {
'src',
];

loadComplete = new PublicPromise();
#loadRequested;
#hasLoaded;
#noInit;
#isInit;
#readyState = 0;

constructor() {
super();
this.loadComplete = new PublicPromise();
}

async load() {
if (this.#hasLoaded) {
this.loadComplete = new PublicPromise();
this.#noInit = true;
}
if (this.#loadRequested) return;

if (this.#hasLoaded) this.loadComplete = new PublicPromise();
this.#hasLoaded = true;

// Wait 1 tick to allow other attributes to be set.
await (this.#loadRequested = Promise.resolve());
this.#loadRequested = null;

this.#readyState = 0;
this.dispatchEvent(new Event('emptied'));

let oldApi = this.api;
this.api = null;

// Wait 1 tick to allow other attributes to be set.
await Promise.resolve();

if (!this.src) {
return;
}

const matches = this.src.match(MATCH_SRC);
const srcId = matches && matches[1];

if (this.#noInit) {
if (this.#isInit) {

this.api = oldApi;
this.api.src = srcId;

} else {
this.#isInit = true;

if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
Expand Down Expand Up @@ -179,7 +177,7 @@ class CloudflareVideoElement extends (globalThis.HTMLElement ?? class {}) {
}
}

this.api.addEventListener('loadstart', () => {
Promise.resolve().then(() => {
this.dispatchEvent(new Event('loadcomplete'));
this.loadComplete.resolve();
});
Expand Down
208 changes: 107 additions & 101 deletions packages/vimeo-video-element/vimeo-video-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,60 @@ import VimeoPlayerAPI from '@vimeo/player/dist/player.es.js';
const EMBED_BASE = 'https://player.vimeo.com/video';
const MATCH_SRC = /vimeo\.com\/(?:video\/)?(\d+)/;

const templateShadowDOM = globalThis.document?.createElement('template');
if (templateShadowDOM) {
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,
playsinline: attrs.playsinline,
preload: attrs.preload ?? 'metadata',
transparent: false,
autopause: attrs.autopause,
};

return `${EMBED_BASE}/${srcId}?${serialize(boolToBinary(params))}`;
}

class VimeoVideoElement extends (globalThis.HTMLElement ?? class {}) {
static getTemplateHTML = getTemplateHTML;
static shadowRootOptions = { mode: 'open' };
static observedAttributes = [
'autoplay',
'controls',
Expand All @@ -39,9 +70,10 @@ class VimeoVideoElement extends (globalThis.HTMLElement ?? class {}) {
'src',
];

loadComplete = new PublicPromise();
#loadRequested;
#hasLoaded;
#noInit;
#options;
#isInit;
#currentTime = 0;
#duration = NaN;
#muted = false;
Expand All @@ -54,22 +86,16 @@ class VimeoVideoElement extends (globalThis.HTMLElement ?? class {}) {
#videoWidth = NaN;
#videoHeight = NaN;

constructor() {
super();

this.attachShadow({ mode: 'open' });
this.shadowRoot.append(templateShadowDOM.content.cloneNode(true));

this.loadComplete = new PublicPromise();
}

async load() {
if (this.#hasLoaded) {
this.loadComplete = new PublicPromise();
this.#noInit = true;
}
if (this.#loadRequested) return;

if (this.#hasLoaded) this.loadComplete = new PublicPromise();
this.#hasLoaded = true;

// Wait 1 tick to allow other attributes to be set.
await (this.#loadRequested = Promise.resolve());
this.#loadRequested = null;

this.#currentTime = 0;
this.#duration = NaN;
this.#muted = false;
Expand All @@ -87,16 +113,13 @@ class VimeoVideoElement extends (globalThis.HTMLElement ?? class {}) {
let oldApi = this.api;
this.api = null;

// Wait 1 tick to allow other attributes to be set.
await Promise.resolve();

if (!this.src) {
return;
}

this.dispatchEvent(new Event('loadstart'));

this.#options = {
const options = {
autoplay: this.autoplay,
controls: this.controls,
loop: this.loop,
Expand Down Expand Up @@ -124,28 +147,26 @@ class VimeoVideoElement extends (globalThis.HTMLElement ?? class {}) {
this.loadComplete.resolve();
};

if (this.#noInit) {
if (this.#isInit) {
this.api = oldApi;
await this.api.loadVideo({
...this.#options,
...options,
url: this.src,
});
await onLoaded();
await this.loadComplete;
return;
}

const matches = this.src.match(MATCH_SRC);
const metaId = matches && matches[1];
const src = `${EMBED_BASE}/${metaId}?${serialize(
boolToBinary(this.#options)
)}`;
let iframe = this.shadowRoot.querySelector('iframe');
if (!iframe) {
iframe = createEmbedIframe({ src });
this.shadowRoot.append(iframe);
this.#isInit = true;

if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes));
}

let iframe = this.shadowRoot.querySelector('iframe');

this.api = new VimeoPlayerAPI(iframe);
const onceLoaded = () => {
this.api.off('loaded', onceLoaded);
Expand Down Expand Up @@ -233,26 +254,21 @@ class VimeoVideoElement extends (globalThis.HTMLElement ?? class {}) {
}

async attributeChangedCallback(attrName, oldValue, newValue) {
if (oldValue === newValue) return;

// This is required to come before the await for resolving loadComplete.
switch (attrName) {
case 'autoplay':
case 'controls':
case 'src': {
if (oldValue !== newValue) {
this.load();
}
this.load();
return;
}
}

await this.loadComplete;

switch (attrName) {
case 'autoplay':
case 'controls': {
if (this.#options[attrName] !== this.hasAttribute(attrName)) {
this.load();
}
break;
}
case 'loop': {
this.api.setLoop(this.loop);
break;
Expand Down Expand Up @@ -430,6 +446,38 @@ class VimeoVideoElement extends (globalThis.HTMLElement ?? class {}) {
}
}

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(props));
}

function boolToBinary(props) {
let p = {};
for (let key in props) {
let val = props[key];
if (val === '') p[key] = 1;
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;
}

/**
* A utility to create Promises with convenient public resolve and reject methods.
* @return {Promise}
Expand All @@ -447,48 +495,6 @@ class PublicPromise extends Promise {
}
}

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; autoplay; encrypted-media; gyroscope; picture-in-picture';

function createEmbedIframe({ src, ...props }) {
return createElement('iframe', {
src,
width: '100%',
height: '100%',
allow,
allowfullscreen: '',
frameborder: 0,
...props,
});
}

function serialize(props) {
return Object.keys(props)
.map((key) => {
if (props[key] == null) return '';
return `${key}=${encodeURIComponent(props[key])}`;
})
.join('&');
}

function boolToBinary(props) {
let p = { ...props };
for (let key in p) {
if (p[key] === false) p[key] = 0;
else if (p[key] === true) p[key] = 1;
}
return p;
}

/**
* Creates a fake `TimeRanges` object.
*
Expand Down
Loading

0 comments on commit c44e755

Please sign in to comment.