From c2bc488366fd28f9a6d00dd28a4f790b5e1400ed Mon Sep 17 00:00:00 2001
From: Wesley Luyten
Date: Thu, 26 Feb 2026 16:00:36 -0600
Subject: [PATCH] feat: add background video preset (#607)
---
packages/html/package.json | 21 +++--------
packages/html/src/define/background/skin.css | 27 ++++++++++++++
packages/html/src/define/background/skin.ts | 34 ++++++++++++++++-
packages/html/src/define/background/video.ts | 1 +
packages/html/src/globals.d.ts | 5 +++
.../html/src/media/background-video/index.ts | 37 ++++++++++++++-----
packages/html/tsdown.config.ts | 18 +++++++++
packages/react/package.json | 18 ++-------
packages/react/src/presets/audio/index.ts | 7 ++++
.../react/src/presets/background/index.ts | 7 ++++
.../react/src/presets/background/skin.css | 22 ++++++++++-
.../react/src/presets/background/skin.tsx | 9 ++++-
packages/react/src/presets/video/index.ts | 7 ++++
packages/sandbox/src/index.html | 2 +
.../templates/html-background/index.html | 13 +++++++
.../sandbox/templates/html-background/main.ts | 17 +++++++++
packages/sandbox/templates/index.html | 2 +
.../templates/react-background/background.tsx | 15 ++++++++
.../templates/react-background/index.html | 13 +++++++
packages/sandbox/vite.config.ts | 2 +
packages/utils/src/dom/attributes.ts | 7 ++++
packages/utils/src/dom/index.ts | 1 +
22 files changed, 241 insertions(+), 44 deletions(-)
create mode 100644 packages/html/src/define/background/skin.css
create mode 100644 packages/html/src/define/background/video.ts
create mode 100644 packages/sandbox/templates/html-background/index.html
create mode 100644 packages/sandbox/templates/html-background/main.ts
create mode 100644 packages/sandbox/templates/react-background/background.tsx
create mode 100644 packages/sandbox/templates/react-background/index.html
create mode 100644 packages/utils/src/dom/attributes.ts
diff --git a/packages/html/package.json b/packages/html/package.json
index afbfe235..7a6cd941 100644
--- a/packages/html/package.json
+++ b/packages/html/package.json
@@ -24,21 +24,6 @@
"development": "./dist/dev/index.js",
"default": "./dist/default/index.js"
},
- "./video/*": {
- "types": "./dist/dev/define/video/*.d.ts",
- "development": "./dist/dev/define/video/*.js",
- "default": "./dist/default/define/video/*.js"
- },
- "./audio/*": {
- "types": "./dist/dev/define/audio/*.d.ts",
- "development": "./dist/dev/define/audio/*.js",
- "default": "./dist/default/define/audio/*.js"
- },
- "./background/*": {
- "types": "./dist/dev/define/background/*.d.ts",
- "development": "./dist/dev/define/background/*.js",
- "default": "./dist/default/define/background/*.js"
- },
"./ui/*": {
"types": "./dist/dev/define/ui/*.d.ts",
"development": "./dist/dev/define/ui/*.js",
@@ -53,6 +38,12 @@
"types": "./dist/dev/define/media/*.d.ts",
"development": "./dist/dev/define/media/*.js",
"default": "./dist/default/define/media/*.js"
+ },
+ "./*.css": "./dist/default/define/*.css",
+ "./*": {
+ "types": "./dist/dev/define/*.d.ts",
+ "development": "./dist/dev/define/*.js",
+ "default": "./dist/default/define/*.js"
}
},
"scripts": {
diff --git a/packages/html/src/define/background/skin.css b/packages/html/src/define/background/skin.css
new file mode 100644
index 00000000..025f827a
--- /dev/null
+++ b/packages/html/src/define/background/skin.css
@@ -0,0 +1,27 @@
+background-video-player {
+ display: contents;
+}
+
+background-video-skin {
+ display: block;
+ position: relative;
+ width: 100%;
+ height: 100%;
+ --media-object-fit: cover;
+ object-fit: var(--media-object-fit);
+}
+
+background-video-skin > [slot="media"] {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: inherit;
+}
+
+background-video-skin > img,
+background-video-skin > picture {
+ width: 100%;
+ height: 100%;
+ object-fit: inherit;
+}
diff --git a/packages/html/src/define/background/skin.ts b/packages/html/src/define/background/skin.ts
index 64f854b0..2e88a663 100644
--- a/packages/html/src/define/background/skin.ts
+++ b/packages/html/src/define/background/skin.ts
@@ -1 +1,33 @@
-// TODO: Implement BackgroundVideoSkinElement and then register it here
+import { ReactiveElement } from '@videojs/element';
+import { namedNodeMapToObject } from '@videojs/utils/dom';
+
+function getTemplateHTML(_attrs: Record) {
+ return /*html*/ `
+
+
+
+ `;
+}
+
+export class BackgroundVideoSkinElement extends ReactiveElement {
+ static readonly tagName = 'background-video-skin';
+ static shadowRootOptions = { mode: 'open' as ShadowRootMode };
+ static getTemplateHTML = getTemplateHTML;
+
+ constructor() {
+ super();
+
+ if (!this.shadowRoot) {
+ this.attachShadow((this.constructor as typeof BackgroundVideoSkinElement).shadowRootOptions);
+ this.shadowRoot!.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes));
+ }
+ }
+}
+
+customElements.define(BackgroundVideoSkinElement.tagName, BackgroundVideoSkinElement);
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [BackgroundVideoSkinElement.tagName]: BackgroundVideoSkinElement;
+ }
+}
diff --git a/packages/html/src/define/background/video.ts b/packages/html/src/define/background/video.ts
new file mode 100644
index 00000000..59561bcd
--- /dev/null
+++ b/packages/html/src/define/background/video.ts
@@ -0,0 +1 @@
+export * from '../media/background-video';
diff --git a/packages/html/src/globals.d.ts b/packages/html/src/globals.d.ts
index b867229b..fd6850af 100644
--- a/packages/html/src/globals.d.ts
+++ b/packages/html/src/globals.d.ts
@@ -1 +1,6 @@
declare const __DEV__: boolean;
+
+declare module '*.css' {
+ const content: string;
+ export default content;
+}
diff --git a/packages/html/src/media/background-video/index.ts b/packages/html/src/media/background-video/index.ts
index c0903b8d..8e969623 100644
--- a/packages/html/src/media/background-video/index.ts
+++ b/packages/html/src/media/background-video/index.ts
@@ -1,4 +1,4 @@
-import { CustomMediaMixin } from '@videojs/core/dom/media/custom-media-element';
+import { namedNodeMapToObject } from '@videojs/utils/dom';
function getTemplateHTML(attrs: Record) {
return /*html*/ `
@@ -12,7 +12,8 @@ function getTemplateHTML(attrs: Record) {
inset: 0;
width: 100%;
height: 100%;
- object-fit: inherit;
+ object-fit: var(--media-object-fit, inherit);
+ object-position: var(--media-object-position, 50% 50%);
}
@@ -20,9 +21,13 @@ function getTemplateHTML(attrs: Record) {
`;
}
-export class BackgroundVideo extends CustomMediaMixin(HTMLElement, { tag: 'video' }) {
+// Don't extend CustomMediaMixin to save some bytes, background videos don't need the full Media API.
+export class BackgroundVideo extends HTMLElement {
static shadowRootOptions = { mode: 'open' as ShadowRootMode };
static getTemplateHTML = getTemplateHTML;
+ static get observedAttributes() {
+ return ['src'];
+ }
constructor() {
super();
@@ -42,15 +47,27 @@ export class BackgroundVideo extends CustomMediaMixin(HTMLElement, { tag: 'video
this.shadowRoot!.innerHTML = getTemplateHTML(attrs);
}
- }
-}
-function namedNodeMapToObject(namedNodeMap: NamedNodeMap) {
- const obj: Record = {};
- for (const attr of namedNodeMap) {
- obj[attr.name] = attr.value;
+ // Neither Chrome or Firefox support setting the muted attribute
+ // after using document.createElement.
+ // Get around this by setting the muted property manually.
+ this.target!.muted = !this.hasAttribute('nomuted');
+ }
+
+ attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string | null): void {
+ if (attrName === 'src' && oldValue !== newValue) {
+ this.target!.src = newValue ?? '';
+ }
+ }
+
+ get target(): HTMLVideoElement | null {
+ return (
+ this.querySelector(':scope > [slot=media]') ??
+ this.querySelector('video') ??
+ this.shadowRoot?.querySelector('video') ??
+ null
+ );
}
- return obj;
}
const VideoAttributes = [
diff --git a/packages/html/tsdown.config.ts b/packages/html/tsdown.config.ts
index 5ba727f4..cd69dec7 100644
--- a/packages/html/tsdown.config.ts
+++ b/packages/html/tsdown.config.ts
@@ -32,6 +32,24 @@ const createConfig = (mode: BuildMode): UserConfig => ({
__DEV__: mode === 'dev' ? 'true' : 'false',
},
dts: mode === 'dev',
+ copy: [
+ {
+ from: 'src/**/*.css',
+ to: `dist/${mode}`,
+ flatten: false,
+ },
+ ],
+ plugins: [
+ {
+ name: 'watch-css',
+ buildStart() {
+ const cssFiles = globSync('src/**/*.css');
+ for (const file of cssFiles) {
+ this.addWatchFile(file);
+ }
+ },
+ },
+ ],
});
export default defineConfig(buildModes.map((mode) => createConfig(mode)));
diff --git a/packages/react/package.json b/packages/react/package.json
index 618d0cbb..61048714 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -28,20 +28,10 @@
"default": "./dist/default/media/*/index.js"
},
"./*.css": "./dist/default/presets/*.css",
- "./audio": {
- "types": "./dist/dev/presets/audio/index.d.ts",
- "development": "./dist/dev/presets/audio/index.js",
- "default": "./dist/default/presets/audio/index.js"
- },
- "./background": {
- "types": "./dist/dev/presets/background/index.d.ts",
- "development": "./dist/dev/presets/background/index.js",
- "default": "./dist/default/presets/background/index.js"
- },
- "./video": {
- "types": "./dist/dev/presets/video/index.d.ts",
- "development": "./dist/dev/presets/video/index.js",
- "default": "./dist/default/presets/video/index.js"
+ "./*": {
+ "types": "./dist/dev/presets/*/index.d.ts",
+ "development": "./dist/dev/presets/*/index.js",
+ "default": "./dist/default/presets/*/index.js"
}
},
"scripts": {
diff --git a/packages/react/src/presets/audio/index.ts b/packages/react/src/presets/audio/index.ts
index d8ec0bfc..280d831d 100644
--- a/packages/react/src/presets/audio/index.ts
+++ b/packages/react/src/presets/audio/index.ts
@@ -1,3 +1,10 @@
+import { createPlayer, features } from '../..';
+
+const { Provider: AudioProvider } = createPlayer({
+ features: features.audio,
+});
+
+export { AudioProvider };
export { Audio, type AudioProps } from '@/media/audio';
export * from './minimal-skin';
export * from './skin';
diff --git a/packages/react/src/presets/background/index.ts b/packages/react/src/presets/background/index.ts
index ead7edd5..17c6ac61 100644
--- a/packages/react/src/presets/background/index.ts
+++ b/packages/react/src/presets/background/index.ts
@@ -1,2 +1,9 @@
+import { createPlayer, features } from '../..';
+
+const { Provider: BackgroundVideoProvider } = createPlayer({
+ features: features.background,
+});
+
+export { BackgroundVideoProvider };
export { BackgroundVideo, type BackgroundVideoProps } from '@/media/background-video';
export * from './skin';
diff --git a/packages/react/src/presets/background/skin.css b/packages/react/src/presets/background/skin.css
index 9c9762b2..743d2571 100644
--- a/packages/react/src/presets/background/skin.css
+++ b/packages/react/src/presets/background/skin.css
@@ -1,3 +1,21 @@
-.media-skin {
- color: var(--media-color, red);
+.media-background-skin {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.media-background-skin > video,
+.media-background-skin > .media {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: inherit;
+}
+
+.media-background-skin > :where(img, picture) {
+ width: 100%;
+ height: 100%;
+ object-fit: inherit;
}
diff --git a/packages/react/src/presets/background/skin.tsx b/packages/react/src/presets/background/skin.tsx
index 2cdf43ba..3e9d897d 100644
--- a/packages/react/src/presets/background/skin.tsx
+++ b/packages/react/src/presets/background/skin.tsx
@@ -1,8 +1,13 @@
+import { cn } from '@videojs/utils/style';
import type { BaseSkinProps } from '../types';
export type BackgroundVideoSkinProps = BaseSkinProps;
export function BackgroundVideoSkin(props: BackgroundVideoSkinProps) {
- const { children } = props;
- return {children}
;
+ const { children, className, ...rest } = props;
+ return (
+
+ {children}
+
+ );
}
diff --git a/packages/react/src/presets/video/index.ts b/packages/react/src/presets/video/index.ts
index 1b5f3ff1..60ff8dc1 100644
--- a/packages/react/src/presets/video/index.ts
+++ b/packages/react/src/presets/video/index.ts
@@ -1,3 +1,10 @@
+import { createPlayer, features } from '../..';
+
+const { Provider: VideoProvider } = createPlayer({
+ features: features.video,
+});
+
+export { VideoProvider };
export { Video, type VideoProps } from '@/media/video';
export * from './minimal-skin';
export * from './minimal-skin.tailwind';
diff --git a/packages/sandbox/src/index.html b/packages/sandbox/src/index.html
index f0af9b8d..7b87b7b4 100644
--- a/packages/sandbox/src/index.html
+++ b/packages/sandbox/src/index.html
@@ -11,7 +11,9 @@
+
+
+
diff --git a/packages/sandbox/templates/html-background/index.html b/packages/sandbox/templates/html-background/index.html
new file mode 100644
index 00000000..580c367b
--- /dev/null
+++ b/packages/sandbox/templates/html-background/index.html
@@ -0,0 +1,13 @@
+
+
+