From fd14f0cb17c2fd1752c82d29252296492ed1568f Mon Sep 17 00:00:00 2001 From: Wesley Luyten Date: Fri, 20 Feb 2026 13:56:32 -0600 Subject: [PATCH] feat: add background video components (#567) --- .../html/src/define/media/background-video.ts | 13 +++ packages/html/src/define/media/hls-video.ts | 2 +- .../html/src/media/background-video/index.ts | 80 +++++++++++++++++++ .../src/{ui => media}/custom-media-element.ts | 0 .../hls-video/index.ts} | 0 .../src/media/background-video/index.tsx | 35 ++++++++ .../react/src/presets/background/index.ts | 1 + 7 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 packages/html/src/define/media/background-video.ts create mode 100644 packages/html/src/media/background-video/index.ts rename packages/html/src/{ui => media}/custom-media-element.ts (100%) rename packages/html/src/{ui/hls-video/hls-video-element.ts => media/hls-video/index.ts} (100%) create mode 100644 packages/react/src/media/background-video/index.tsx diff --git a/packages/html/src/define/media/background-video.ts b/packages/html/src/define/media/background-video.ts new file mode 100644 index 00000000..01ee8581 --- /dev/null +++ b/packages/html/src/define/media/background-video.ts @@ -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; + } +} diff --git a/packages/html/src/define/media/hls-video.ts b/packages/html/src/define/media/hls-video.ts index 5c80ba88..2b8e3780 100644 --- a/packages/html/src/define/media/hls-video.ts +++ b/packages/html/src/define/media/hls-video.ts @@ -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'; diff --git a/packages/html/src/media/background-video/index.ts b/packages/html/src/media/background-video/index.ts new file mode 100644 index 00000000..3f44da5a --- /dev/null +++ b/packages/html/src/media/background-video/index.ts @@ -0,0 +1,80 @@ +import { CustomMediaMixin } from '../custom-media-element'; + +function getTemplateHTML(attrs: Record) { + return /*html*/ ` + + + + `; +} + +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 = {}; + 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 { + 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; +} diff --git a/packages/html/src/ui/custom-media-element.ts b/packages/html/src/media/custom-media-element.ts similarity index 100% rename from packages/html/src/ui/custom-media-element.ts rename to packages/html/src/media/custom-media-element.ts diff --git a/packages/html/src/ui/hls-video/hls-video-element.ts b/packages/html/src/media/hls-video/index.ts similarity index 100% rename from packages/html/src/ui/hls-video/hls-video-element.ts rename to packages/html/src/media/hls-video/index.ts diff --git a/packages/react/src/media/background-video/index.tsx b/packages/react/src/media/background-video/index.tsx new file mode 100644 index 00000000..48f249a7 --- /dev/null +++ b/packages/react/src/media/background-video/index.tsx @@ -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 {} + +export const BackgroundVideo = forwardRef(function BackgroundVideo( + { children, ...props }, + ref +) { + const setMedia = useMediaRegistration(); + + const mediaRef = useCallback( + (el: HTMLVideoElement | null) => { + setMedia?.(el); + }, + [setMedia] + ); + + const composedRef = useComposedRefs(ref, mediaRef); + + return ( + + ); +}); + +export namespace BackgroundVideo { + export type Props = BackgroundVideoProps; +} diff --git a/packages/react/src/presets/background/index.ts b/packages/react/src/presets/background/index.ts index 966e7046..ead7edd5 100644 --- a/packages/react/src/presets/background/index.ts +++ b/packages/react/src/presets/background/index.ts @@ -1 +1,2 @@ +export { BackgroundVideo, type BackgroundVideoProps } from '@/media/background-video'; export * from './skin';