feat: add background video preset (#607)

This commit is contained in:
Wesley Luyten
2026-02-26 16:00:36 -06:00
committed by GitHub
parent 0a641e90e2
commit c2bc488366
22 changed files with 241 additions and 44 deletions
@@ -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;
}
+33 -1
View File
@@ -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<string, string>) {
return /*html*/ `
<media-container>
<slot name="media" slot="media"></slot>
</media-container>
`;
}
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;
}
}
@@ -0,0 +1 @@
export * from '../media/background-video';
+5
View File
@@ -1 +1,6 @@
declare const __DEV__: boolean;
declare module '*.css' {
const content: string;
export default content;
}
@@ -1,4 +1,4 @@
import { CustomMediaMixin } from '@videojs/core/dom/media/custom-media-element';
import { namedNodeMapToObject } from '@videojs/utils/dom';
function getTemplateHTML(attrs: Record<string, string>) {
return /*html*/ `
@@ -12,7 +12,8 @@ function getTemplateHTML(attrs: Record<string, string>) {
inset: 0;
width: 100%;
height: 100%;
object-fit: inherit;
object-fit: var(--media-object-fit, inherit);
object-position: var(--media-object-position, 50% 50%);
}
</style>
<slot></slot>
@@ -20,9 +21,13 @@ function getTemplateHTML(attrs: Record<string, string>) {
`;
}
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<string, string> = {};
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 = [