Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/sandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import { SOURCES } from '@app/shared/sources';

See `templates/html-video/main.ts` for a minimal reference, or `templates/react-video/main.tsx` for a React one.

Each html template spells out its player the way a consumer would — the player element, the skin, and the media inside it. `createHtmlSandbox` only supplies what the shell varies: `skinTag`, the live `playerTag`, the source and poster URLs, and the attributes the Options panel controls. The markup inside `render` can be copied as is, with `${skinTag}` replaced by a skin's tag such as `video-skin`.

## Syncing changes back to templates

When you've made improvements in `src/` that should become the new baseline:
Expand Down
90 changes: 33 additions & 57 deletions apps/sandbox/app/shared/html/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { escapeHtml } from '@videojs/utils/string';

import { applyCaptionTracks } from '../captions';
import { findMediaTag } from '../media-element';
import { PLAYER_FRAME_CLASSES } from '../player-frame';
import {
getDirection,
getInitialPlaybackOverrides,
Expand All @@ -29,11 +28,19 @@ export type HtmlSandboxPlayer = 'video' | 'audio' | 'background';
/** A source assigned as an object, for what a `src` attribute cannot carry: tokens, license servers, engine options. */
export type HtmlSandboxSource = MuxSource | ({ src: string } & PlaybackOverrides);

/** What a template's media markup can read: the shell's selections plus what the runtime derived from them. */
/** What a template's markup can read: the shell's selections plus what the runtime derived from them. */
export interface HtmlSandboxContext {
readonly state: Readonly<SandboxState>;
/** The live player and skin variants are in use for this render. */
readonly live: boolean;
/** The player element for this render, such as `video-player` or `live-audio-player`. */
readonly playerTag: string;
/** The skin element the shell selected and the runtime registered, such as `video-skin` or `audio-minimal-skin`. */
readonly skinTag: string;
/** The source's poster URL, safe to interpolate into an attribute, or empty when it has none. */
readonly poster: string;
/** A tiny blurred rendition of the poster for a blur-up placeholder, attribute-safe, or empty when there is none. */
readonly placeholder: string;
/** The selected source's plain URL, or empty when it has none. */
readonly url: string;
/** ` src="…"` for the media element, or empty when the source has to be assigned as an object after render. */
Expand All @@ -52,19 +59,13 @@ export interface HtmlSandboxOptions {
readonly player: HtmlSandboxPlayer;
/** Switch to the live player and skin while the selected source is live. Leave off for media that cannot play one. */
readonly live?: boolean;
/**
* How the poster reaches the skin. `image` slots the source's poster image after the media. `derived` hands the URL
* to the player and slots a blurred placeholder before the media instead, for media that derives its poster from
* `src`. Neither renders by default.
*/
readonly poster?: 'image' | 'derived';
/**
* Fold the query-string playback overrides into the initial source. That forces the object form, so the engine is
* built with them rather than reconfigured afterwards.
*/
readonly playbackOverrides?: boolean;
/** The media element and any media components beside it, inside the skin. */
readonly media: (context: HtmlSandboxContext) => string;
/** The whole player as a consumer would write it: the player element, the skin, and the media inside it. */
readonly render: (context: HtmlSandboxContext) => string;
/** Runs once the markup is in the document, for what an attribute cannot carry: assigning `context.source`. */
readonly attach?: (context: HtmlSandboxContext) => void;
}
Expand Down Expand Up @@ -120,62 +121,36 @@ function describeSource(state: SandboxState, playbackOverrides: boolean) {
};
}

function createContext(options: HtmlSandboxOptions, state: SandboxState, live: boolean): HtmlSandboxContext {
function playerTagFor(player: HtmlSandboxPlayer, live: boolean): string {
if (player === 'background') return 'background-video-player';

return live ? `live-${player}-player` : `${player}-player`;
}

function createContext(
options: HtmlSandboxOptions,
state: SandboxState,
live: boolean,
skinTag: string
): HtmlSandboxContext {
const { url, src, source } = describeSource(state, options.playbackOverrides === true);

return {
state,
live,
playerTag: playerTagFor(options.player, live),
skinTag,
url,
src,
source,
attrs: renderMediaAttrs(state),
chapters: renderChapters(getChapters(state.source)),
storyboard: renderStoryboard(getStoryboardSrc(state.source)),
poster: escapeHtml(getPosterSrc(state.source) ?? ''),
placeholder: escapeHtml(getPlaceholderSrc(state.source) ?? ''),
};
}

function renderPlayer(options: HtmlSandboxOptions, skinTag: string, context: HtmlSandboxContext): string {
const { player, poster } = options;
const { live, state } = context;
const posterSrc = poster === undefined ? undefined : getPosterSrc(state.source);
const placeholder = poster === 'derived' ? getPlaceholderSrc(state.source) : undefined;
const children = html`
${placeholder ? `<img slot="poster" alt="" crossorigin style="background: url('${escapeHtml(placeholder)}') var(--media-object-position, center) / contain no-repeat">` : ''}
${options.media(context)}
${poster === 'image' && posterSrc ? html`<img slot="poster" src="${escapeHtml(posterSrc)}" alt="Video poster" crossorigin />` : ''}
`;

if (player === 'background') {
return html`
<background-video-player>
<${skinTag}>${children}</${skinTag}>
</background-video-player>
`;
}

if (player === 'audio') {
const playerTag = live ? 'live-audio-player' : 'audio-player';

return html`
<div class="${PLAYER_FRAME_CLASSES.audio}">
<${playerTag}>
<${skinTag}>${children}</${skinTag}>
</${playerTag}>
</div>
`;
}

const playerTag = live ? 'live-video-player' : 'video-player';
const posterAttr = poster === 'derived' && posterSrc ? ` poster="${escapeHtml(posterSrc)}"` : '';

return html`
<${playerTag}${posterAttr}>
<${skinTag} class="${PLAYER_FRAME_CLASSES.video}">${children}</${skinTag}>
</${playerTag}>
`;
}

function getRoot(): HTMLElement {
const root = document.getElementById('root');
if (!root) throw new Error('The sandbox page has no #root element.');
Expand All @@ -184,9 +159,10 @@ function getRoot(): HTMLElement {
}

/**
* Mount a preview page: read the shell's selections, load the skin they name, render the player around the template's
* media markup, and render again as the shell streams changes. A locale change applies in place through `<media-i18n>`
* once the player is up; a direction change renders again, since the provider owns the pinned `dir`.
* Mount a preview page: read the shell's selections, load the skin they name, render the template's player markup with
* the tags and sources they resolve to, and render again as the shell streams changes. A locale change applies in place
* through `<media-i18n>` once the player is up; a direction change renders again, since the provider owns the pinned
* `dir`.
*/
export function createHtmlSandbox(options: HtmlSandboxOptions): void {
const state = readSandboxState('html');
Expand All @@ -202,11 +178,11 @@ export function createHtmlSandbox(options: HtmlSandboxOptions): void {
const skinTag = await loadLatest(() => loadSkinTag(options.player, state, live));
if (!skinTag) return;

const context = createContext(options, state, live);
const context = createContext(options, state, live, skinTag);

const template = document.createElement('template');

template.innerHTML = wrapSandboxHtmlI18n(renderPlayer(options, skinTag, context));
template.innerHTML = wrapSandboxHtmlI18n(options.render(context));

// Subtitle tracks are the page's to add, so a template never has to spell them out. They go in while the markup is
// still inert: a custom media element reads its tracks when it upgrades, not when children arrive later.
Expand Down
5 changes: 3 additions & 2 deletions apps/sandbox/app/shared/player-frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ export function defaultPlayerWidth(player: MediaPlayer): number {
}

/**
* How a preview frames its player: centred, and capped by the shell's width control through `--sandbox-player-width`,
* with the skin's own cap when a page is opened without one.
* How the React skin components frame their player: centred, and capped by the shell's width control through
* `--sandbox-player-width`, with the skin's own cap when a page is opened without one. The html templates write the
* plain `max-w-4xl` and `max-w-xl` classes a consumer would, and `styles.css` caps those the same way.
*/
export const PLAYER_FRAME_CLASSES = {
// Keep centred controls on device pixels instead of the fractional height produced by aspect-ratio.
Expand Down
18 changes: 18 additions & 0 deletions apps/sandbox/app/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,24 @@
}
}

/*
* The shell's width control sizes the player through `--sandbox-player-width` (see `shared/player-frame.ts`). The html
* templates frame their player with the classes a consumer would write, `max-w-4xl` and `max-w-xl`, and these unlayered
* rules cap them at the control's width instead, falling back to those same widths when a page is opened without one.
* React pages take the equivalent classes through `VideoSkinComponent` and `AudioSkinComponent`. The CDN page puts
* `<media-i18n>` inside the player rather than around it, hence the second selector.
*/
#root :is(video-player, live-video-player) > :not(media-i18n),
#root :is(video-player, live-video-player) > media-i18n > * {
max-width: var(--sandbox-player-width, 56rem);
/* Keep centred controls on device pixels instead of the fractional height produced by aspect-ratio. */
height: round(nearest, calc(min(var(--sandbox-player-width, 56rem), 100vw - 1rem) * 9 / 16), 2px) !important;
}

#root :has(> :is(audio-player, live-audio-player)) {
max-width: var(--sandbox-player-width, 36rem);
}

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
Expand Down
5 changes: 2 additions & 3 deletions apps/sandbox/templates/cdn/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { ensureCdnSandboxLocale } from '@app/shared/i18n/cdn-sandbox-locales';
import { syncDocumentLocale } from '@app/shared/i18n/document-locale';
import type { SandboxLocaleTag } from '@app/shared/i18n/locale-meta';
import { findMediaTag } from '@app/shared/media-element';
import { PLAYER_FRAME_CLASSES } from '@app/shared/player-frame';
import {
getDirection,
getInitialLocale,
Expand Down Expand Up @@ -364,7 +363,7 @@ async function render() {

if (descriptor.player === 'audio') {
root.innerHTML = html`
<div class="${PLAYER_FRAME_CLASSES.audio}">
<div class="mx-auto w-full max-w-xl">
${wrapCdnPlayerI18n(
playerTag,
html`
Expand All @@ -380,7 +379,7 @@ async function render() {
}

const skin = html`
<${skinTag} class="${PLAYER_FRAME_CLASSES.video}">
<${skinTag} class="mx-auto aspect-video max-w-4xl">
<${mediaTag} ${mediaClassAttr} ${sourceAttr} ${mediaAttrs} playsinline ${crossoriginAttr}>
${skinnedVideo ? renderChapters(getChapters(state.source)) : ''}
${renderStoryboard(storyboard)}
Expand Down
10 changes: 9 additions & 1 deletion apps/sandbox/templates/html-audio/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,13 @@ import { createHtmlSandbox, html } from '@app/shared/html/sandbox';

createHtmlSandbox({
player: 'audio',
media: ({ src, attrs }) => html`<audio${src} ${attrs} crossorigin></audio>`,
render: ({ skinTag, src, attrs }) => html`
<div class="mx-auto w-full max-w-xl">
<audio-player>
<${skinTag}>
<audio${src} ${attrs} crossorigin></audio>
</${skinTag}>
</audio-player>
</div>
`,
});
8 changes: 7 additions & 1 deletion apps/sandbox/templates/html-background-video/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,11 @@ import { BACKGROUND_VIDEO_SRC } from '@app/shared/sources';

createHtmlSandbox({
player: 'background',
media: () => html`<background-video src="${BACKGROUND_VIDEO_SRC}" crossorigin></background-video>`,
render: () => html`
<background-video-player>
<background-video-skin>
<background-video src="${BACKGROUND_VIDEO_SRC}" crossorigin></background-video>
</background-video-skin>
</background-video-player>
`,
});
9 changes: 7 additions & 2 deletions apps/sandbox/templates/html-cloudflare-video/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { CLOUDFLARE_VIDEO_SRC } from '@app/shared/sources';

createHtmlSandbox({
player: 'video',
media: () =>
html`<cloudflare-video class="block h-full w-full" src="${CLOUDFLARE_VIDEO_SRC}" playsinline></cloudflare-video>`,
render: ({ skinTag }) => html`
<video-player>
<${skinTag} class="mx-auto aspect-video max-w-4xl">
<cloudflare-video class="block h-full w-full" src="${CLOUDFLARE_VIDEO_SRC}" playsinline></cloudflare-video>
</${skinTag}>
</video-player>
`,
});
18 changes: 11 additions & 7 deletions apps/sandbox/templates/html-dash-video/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import { createHtmlSandbox, html } from '@app/shared/html/sandbox';

createHtmlSandbox({
player: 'video',
poster: 'image',
media: ({ src, attrs, storyboard }) => html`
<dash-video${src} ${attrs} playsinline crossorigin>${storyboard}</dash-video>
<!-- Mux Data is an opt-in media component. It hands the dash.js engine to the Mux Data
SDK, so views carry stream-level detail. These streams aren't Mux-hosted, so the
sandbox env key is what attributes the views. -->
<mux-data player-software-name="dash-video" env-key="o9b7ge20gji31ao0rub18505f"></mux-data>
render: ({ skinTag, src, attrs, storyboard, poster }) => html`
<video-player>
<${skinTag} class="mx-auto aspect-video max-w-4xl">
<dash-video${src} ${attrs} playsinline crossorigin>${storyboard}</dash-video>
<!-- Mux Data is an opt-in media component. It hands the dash.js engine to the Mux Data
SDK, so views carry stream-level detail. These streams aren't Mux-hosted, so the
sandbox env key is what attributes the views. -->
<mux-data player-software-name="dash-video" env-key="o9b7ge20gji31ao0rub18505f"></mux-data>
${poster ? html`<img slot="poster" src="${poster}" alt="Video poster" crossorigin />` : ''}
</${skinTag}>
</video-player>
`,
});
10 changes: 9 additions & 1 deletion apps/sandbox/templates/html-hls-audio/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,13 @@ import { createHtmlSandbox, html } from '@app/shared/html/sandbox';
createHtmlSandbox({
player: 'audio',
live: true,
media: ({ src, attrs }) => html`<hls-audio${src} ${attrs} crossorigin></hls-audio>`,
render: ({ playerTag, skinTag, src, attrs }) => html`
<div class="mx-auto w-full max-w-xl">
<${playerTag}>
<${skinTag}>
<hls-audio${src} ${attrs} crossorigin></hls-audio>
</${skinTag}>
</${playerTag}>
</div>
`,
});
8 changes: 7 additions & 1 deletion apps/sandbox/templates/html-hls-background-video/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,11 @@ import { createHtmlSandbox, html } from '@app/shared/html/sandbox';

createHtmlSandbox({
player: 'background',
media: ({ src }) => html`<hls-background-video${src} crossorigin></hls-background-video>`,
render: ({ src }) => html`
<background-video-player>
<background-video-skin>
<hls-background-video${src} crossorigin></hls-background-video>
</background-video-skin>
</background-video-player>
`,
});
16 changes: 10 additions & 6 deletions apps/sandbox/templates/html-hls-video/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import { createHtmlSandbox, html } from '@app/shared/html/sandbox';
createHtmlSandbox({
player: 'video',
live: true,
poster: 'image',
media: ({ src, attrs, chapters, storyboard }) => html`
<hls-video${src} ${attrs} playsinline crossorigin>
${chapters}
${storyboard}
</hls-video>
render: ({ playerTag, skinTag, src, attrs, chapters, storyboard, poster }) => html`
<${playerTag}>
<${skinTag} class="mx-auto aspect-video max-w-4xl">
<hls-video${src} ${attrs} playsinline crossorigin>
${chapters}
${storyboard}
</hls-video>
${poster ? html`<img slot="poster" src="${poster}" alt="Video poster" crossorigin />` : ''}
</${skinTag}>
</${playerTag}>
`,
});
16 changes: 10 additions & 6 deletions apps/sandbox/templates/html-hlsjs-video/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@ import { createHtmlSandbox, html } from '@app/shared/html/sandbox';
createHtmlSandbox({
player: 'video',
live: true,
poster: 'image',
// A source carrying DRM license servers has no room in the `src` attribute, so
// it is assigned as an object below instead. Query-string playback overrides
// need the object for the same reason, and need it before the first load so the
// engine is built with them rather than reconfigured afterwards.
playbackOverrides: true,
media: ({ src, attrs, chapters, storyboard }) => html`
<hlsjs-video${src} ${attrs} playsinline crossorigin>
${chapters}
${storyboard}
</hlsjs-video>
render: ({ playerTag, skinTag, src, attrs, chapters, storyboard, poster }) => html`
<${playerTag}>
<${skinTag} class="mx-auto aspect-video max-w-4xl">
<hlsjs-video${src} ${attrs} playsinline crossorigin>
${chapters}
${storyboard}
</hlsjs-video>
${poster ? html`<img slot="poster" src="${poster}" alt="Video poster" crossorigin />` : ''}
</${skinTag}>
</${playerTag}>
`,
attach: ({ source }) => {
if (source) document.querySelector('hlsjs-video')!.source = source;
Expand Down
Loading
Loading