feat: add background video components (#567)

This commit is contained in:
Wesley Luyten
2026-02-20 13:56:32 -06:00
committed by GitHub
parent 604f40605b
commit fd14f0cb17
7 changed files with 130 additions and 1 deletions
@@ -0,0 +1,13 @@
import { BackgroundVideo } from '../../media/background-video';
export class BackgroundVideoElement extends BackgroundVideo {
static readonly tagName = 'background-video';
}
customElements.define(BackgroundVideoElement.tagName, BackgroundVideoElement);
declare global {
interface HTMLElementTagNameMap {
[BackgroundVideoElement.tagName]: BackgroundVideoElement;
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { HlsVideo } from '../../ui/hls-video/hls-video-element';
import { HlsVideo } from '../../media/hls-video';
export class HlsVideoElement extends HlsVideo {
static readonly tagName = 'hls-video';
@@ -0,0 +1,80 @@
import { CustomMediaMixin } from '../custom-media-element';
function getTemplateHTML(attrs: Record<string, string>) {
return /*html*/ `
<style>
:host {
position: relative;
}
video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: inherit;
}
</style>
<slot></slot>
<video ${serializeAttributes(attrs)}></video>
`;
}
export class BackgroundVideo extends CustomMediaMixin(HTMLElement, { tag: 'video' }) {
static shadowRootOptions = { mode: 'open' as ShadowRootMode };
static getTemplateHTML = getTemplateHTML;
constructor() {
super();
if (!this.shadowRoot) {
this.attachShadow((this.constructor as typeof BackgroundVideo).shadowRootOptions);
const attrs = {
...namedNodeMapToObject(this.attributes),
...(!this.hasAttribute('nomuted') && { muted: '' }),
...(!this.hasAttribute('noloop') && { loop: '' }),
...(!this.hasAttribute('noautoplay') && { autoplay: '' }),
playsinline: '',
disableremoteplayback: '',
disablepictureinpicture: '',
};
this.shadowRoot!.innerHTML = getTemplateHTML(attrs);
}
}
}
function namedNodeMapToObject(namedNodeMap: NamedNodeMap) {
const obj: Record<string, string> = {};
for (const attr of namedNodeMap) {
obj[attr.name] = attr.value;
}
return obj;
}
const VideoAttributes = [
'autoplay',
'controls',
'controlslist',
'crossorigin',
'disablepictureinpicture',
'disableremoteplayback',
'loop',
'muted',
'playsinline',
'preload',
] as const;
function serializeAttributes(attrs: Record<string, string>): string {
let html = '';
for (const key in attrs) {
// Skip forwarding non native video attributes.
if (!VideoAttributes.includes(key as any)) continue;
const value = attrs[key];
if (value === '') html += ` ${key}`;
else html += ` ${key}="${value}"`;
}
return html;
}
@@ -0,0 +1,35 @@
'use client';
import type { VideoHTMLAttributes } from 'react';
import { forwardRef, useCallback } from 'react';
import { useMediaRegistration } from '../../player/context';
import { useComposedRefs } from '../../utils/use-composed-refs';
export interface BackgroundVideoProps extends VideoHTMLAttributes<HTMLVideoElement> {}
export const BackgroundVideo = forwardRef<HTMLVideoElement, BackgroundVideoProps>(function BackgroundVideo(
{ children, ...props },
ref
) {
const setMedia = useMediaRegistration();
const mediaRef = useCallback(
(el: HTMLVideoElement | null) => {
setMedia?.(el);
},
[setMedia]
);
const composedRef = useComposedRefs(ref, mediaRef);
return (
<video ref={composedRef} muted autoPlay loop playsInline disableRemotePlayback disablePictureInPicture {...props}>
{children}
</video>
);
});
export namespace BackgroundVideo {
export type Props = BackgroundVideoProps;
}
@@ -1 +1,2 @@
export { BackgroundVideo, type BackgroundVideoProps } from '@/media/background-video';
export * from './skin';