Skip to content

Commit

Permalink
feat: add ability to choose a Media Theme via attribute (#269)
Browse files Browse the repository at this point in the history
  • Loading branch information
luwes authored Jul 12, 2022
1 parent 94d000a commit 77d0386
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 122 deletions.
7 changes: 7 additions & 0 deletions packages/mux-player/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ export const getStoryboardURLFromPlaybackId = (
})}`;
};

export function castThemeName(themeName?: string): string | undefined {
if (themeName && /^media-theme-[\w-]+$/.test(themeName)) {
return themeName;
}
return undefined;
}

const attrToPropNameMap: Record<string, string> = {
crossorigin: 'crossOrigin',
playsinline: 'playsInline',
Expand Down
50 changes: 50 additions & 0 deletions packages/mux-player/src/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,49 @@ export class TemplateResult {
}
}

const stringsCache = new Map();
const defaultProcessor = createProcessor(processPart);
export function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult {
const staticStrings: any = [''];
const dynamicValues: any[] = [];
let staticValues;
let hasStatics = false;

// Here the unsafe static values are moved from the string expressions
// to the static strings so they can be used in the cache key and later
// be used to generate the HTML via the <template> element.
const join = (strs: TemplateStringsArray, vals: any[] = []) => {
staticStrings[staticStrings.length - 1] = staticStrings[staticStrings.length - 1] + strs[0];

vals.forEach((dynamicValue, i) => {
if ((staticValues = dynamicValue?.$static$) !== undefined) {
staticValues.forEach((staticValue: TemplateResult) => {
join(staticValue.strings, staticValue.values);
});

staticStrings[staticStrings.length - 1] = staticStrings[staticStrings.length - 1] + strs[i + 1];
hasStatics = true;
} else {
dynamicValues.push(dynamicValue);
staticStrings.push(strs[i + 1]);
}
});
};

join(strings, values);

if (hasStatics) {
// Tagged template literals with the same static strings return the same
// TemplateStringsArray, aka they are cached. emulate this behavior w/ a Map.
const key = staticStrings.join('$$html$$');
strings = stringsCache.get(key);
if (strings === undefined) {
(staticStrings as any).raw = staticStrings;
stringsCache.set(key, (strings = staticStrings));
}
values = dynamicValues;
}

return new TemplateResult(strings, values, defaultProcessor);
}

Expand All @@ -163,3 +204,12 @@ export function createTemplateInstance(content: string, props?: any) {
template.innerHTML = content;
return new TemplateInstance(template, props);
}

export const unsafeStatic = (...values: any[]) => ({
['$static$']: values.map((value) => {
if (value instanceof TemplateResult) return value;
// Only allow word characters and dashes for security.
if (!/\w-/.test(value)) return { strings: [] };
return { strings: [value] };
}),
});
26 changes: 15 additions & 11 deletions packages/mux-player/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import '@mux/playback-core';
// @ts-ignore
import { MediaController } from 'media-chrome';
import MediaThemeMux from './media-theme-mux/media-theme-mux';
import MuxVideoElement, { MediaError } from '@mux/mux-video';
import { Metadata, StreamTypes } from '@mux/playback-core';
import VideoApiElement, { initVideoApi } from './video-api';
Expand All @@ -21,8 +20,6 @@ export type Tokens = {
storyboard?: string;
};

type MediaController = Element & { media: HTMLVideoElement };

const streamTypeValues = Object.values(StreamTypes);

const SMALL_BREAKPOINT = 700;
Expand Down Expand Up @@ -85,6 +82,7 @@ function getProps(el: MuxPlayerElement, state?: any): MuxTemplateProps {
// it's used if/when it's been explicitly set "from the outside"
// (See template.ts for additional context) (CJP)
poster: el.getAttribute('poster'),
theme: el.getAttribute('theme'),
thumbnailTime: !el.tokens.thumbnail && el.thumbnailTime,
autoplay: el.autoplay,
crossOrigin: el.crossOrigin,
Expand Down Expand Up @@ -175,19 +173,25 @@ class MuxPlayerElement extends VideoApiElement {
// Fixes a bug in React where mux-player's CE children were not upgraded yet.
// These lines ensure the rendered mux-video and media-controller are upgraded,
// even before they are connected to the main document.
customElements.upgrade(this.theme as Node);
if (!(this.theme instanceof MediaThemeMux)) {
logger.error('<media-theme-mux> failed to upgrade!');
try {
customElements.upgrade(this.theme as Node);
if (!(this.theme instanceof HTMLElement)) throw '';
} catch (error) {
logger.error(`<${this.theme?.localName}> failed to upgrade!`);
}

customElements.upgrade(this.media as Node);
if (!(this.media instanceof MuxVideoElement)) {
try {
customElements.upgrade(this.media as Node);
if (!(this.media instanceof MuxVideoElement)) throw '';
} catch (error) {
logger.error('<mux-video> failed to upgrade!');
}

customElements.upgrade(this.mediaController as Node);
if (!(this.mediaController instanceof MediaController)) {
logger.error('<media-controller> failed to upgrade!');
try {
customElements.upgrade(this.mediaController as Node);
if (!(this.mediaController instanceof MediaController)) throw '';
} catch (error) {
logger.error(`<media-controller> failed to upgrade!`);
}

initVideoApi(this);
Expand Down
112 changes: 52 additions & 60 deletions packages/mux-player/src/media-theme-mux/media-theme-mux.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'media-chrome';
import { MediaTheme } from 'media-chrome';
import { html, render } from '../html';
import '../media-chrome/time-display';

Expand All @@ -14,34 +14,68 @@ const MediaChromeSizes = {
};

type ThemeMuxTemplateProps = {
streamType: string;
streamType: string | null;
audio: boolean;
playerSize: string;
playerSize: string | null;
defaultHiddenCaptions: boolean;
forwardSeekOffset: number;
backwardSeekOffset: number;
forwardSeekOffset: string | null;
backwardSeekOffset: string | null;
};

const template = (props: ThemeMuxTemplateProps) => html`
<style>
${cssStr}
</style>
export default class MediaThemeMux extends MediaTheme {
static get observedAttributes() {
return [
'audio',
'stream-type',
'player-size',
'default-hidden-captions',
'forward-seek-offset',
'backward-seek-offset',
];
}

<media-controller audio="${props.audio || false}" class="size-${props.playerSize}">
<slot name="media" slot="media"></slot>
<media-loading-indicator slot="centered-chrome" no-auto-hide></media-loading-indicator>
${ChromeRenderer(props)}
<slot></slot>
</media-controller>
`;
attributeChangedCallback() {
this.render();
}

render() {
const props = {
audio: this.hasAttribute('audio'),
streamType: this.getAttribute('stream-type'),
playerSize: this.getAttribute('player-size'),
defaultHiddenCaptions: this.hasAttribute('default-hidden-captions'),
forwardSeekOffset: this.getAttribute('forward-seek-offset'),
backwardSeekOffset: this.getAttribute('backward-seek-offset'),
};

render(
html`
<style>
${cssStr}
</style>
<media-controller audio="${props.audio || false}" class="size-${props.playerSize}">
<slot name="media" slot="media"></slot>
<media-loading-indicator slot="centered-chrome" no-auto-hide></media-loading-indicator>
${ChromeRenderer(props)}
<slot></slot>
</media-controller>
`,
this.shadowRoot as Node
);
}
}

if (!customElements.get('media-theme-mux')) {
customElements.define('media-theme-mux', MediaThemeMux);
}

const ChromeRenderer = (props: ThemeMuxTemplateProps) => {
const { streamType, playerSize, audio } = props;
if (audio) {
switch (streamType) {
case StreamTypes.LIVE:
case StreamTypes.LL_LIVE: {
return AudioLiveChrome(props);
return AudioLiveChrome();
}
case StreamTypes.DVR:
case StreamTypes.LL_DVR: {
Expand Down Expand Up @@ -179,7 +213,7 @@ export const AudioDvrChrome = (props: ThemeMuxTemplateProps) => html`
</media-control-bar>
`;

export const AudioLiveChrome = (_props: ThemeMuxTemplateProps) => html`
export const AudioLiveChrome = () => html`
<media-control-bar>
${MediaPlayButton()}
<slot name="seek-to-live-button"></slot>
Expand Down Expand Up @@ -351,45 +385,3 @@ export const DvrChromeLarge = (props: ThemeMuxTemplateProps) => html`
<div class="mxp-padding-2"></div>
</media-control-bar>
`;

function getProps(el: MediaThemeMux, state?: any): ThemeMuxTemplateProps {
return {
audio: el.hasAttribute('audio'),
streamType: el.getAttribute('stream-type'),
playerSize: el.getAttribute('player-size'),
defaultHiddenCaptions: el.hasAttribute('default-hidden-captions'),
forwardSeekOffset: el.getAttribute('forward-seek-offset'),
backwardSeekOffset: el.getAttribute('backward-seek-offset'),
...state,
};
}

class MediaThemeMux extends HTMLElement {
static get observedAttributes() {
return [
'audio',
'stream-type',
'player-size',
'default-hidden-captions',
'forward-seek-offset',
'backward-seek-offset',
];
}

constructor() {
super();

this.attachShadow({ mode: 'open' });
render(template(getProps(this)), this.shadowRoot as Node);
}

attributeChangedCallback() {
render(template(getProps(this)), this.shadowRoot as Node);
}
}

if (!customElements.get('media-theme-mux')) {
customElements.define('media-theme-mux', MediaThemeMux);
}

export default MediaThemeMux;
Loading

5 comments on commit 77d0386

@vercel
Copy link

@vercel vercel bot commented on 77d0386 Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

elements-demo-svelte-kit – ./examples/svelte-kit

elements-demo-svelte-kit-mux.vercel.app
elements-demo-svelte-kit.vercel.app
elements-demo-svelte-kit-git-main-mux.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 77d0386 Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

elements-demo-nextjs – ./examples/nextjs-with-typescript

elements-demo-nextjs-mux.vercel.app
elements-demo-nextjs.vercel.app
elements-demo-nextjs-git-main-mux.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 77d0386 Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

elements-demo-vue – ./examples/vue-with-typescript

elements-demo-vue-mux.vercel.app
elements-demo-vue-git-main-mux.vercel.app
elements-demo-vue.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 77d0386 Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

elements-demo-create-react-app – ./examples/create-react-app-with-typescript

elements-demo-create-react-app.vercel.app
elements-demo-create-react-app-git-main-mux.vercel.app
elements-demo-create-react-app-mux.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 77d0386 Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

elements-demo-vanilla – ./examples/vanilla-ts-esm

elements-demo-vanilla-mux.vercel.app
elements-demo-vanilla.vercel.app
elements-demo-vanilla-git-main-mux.vercel.app

Please sign in to comment.