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 @@ + + + + + + Sandbox — HTML Background + + + +
+ + + diff --git a/packages/sandbox/templates/html-background/main.ts b/packages/sandbox/templates/html-background/main.ts new file mode 100644 index 00000000..1c65ceec --- /dev/null +++ b/packages/sandbox/templates/html-background/main.ts @@ -0,0 +1,17 @@ +// HTML sandbox — Background video player +// http://localhost:5173/html-background/ + +import '@videojs/html/background/skin.css'; +import '@videojs/html/background/player'; +import '@videojs/html/background/skin'; +import '@videojs/html/background/video'; + +const html = String.raw; + +document.getElementById('root')!.innerHTML = html` + + + + + +`; diff --git a/packages/sandbox/templates/index.html b/packages/sandbox/templates/index.html index f0af9b8d..7b87b7b4 100644 --- a/packages/sandbox/templates/index.html +++ b/packages/sandbox/templates/index.html @@ -11,7 +11,9 @@ diff --git a/packages/sandbox/templates/react-background/background.tsx b/packages/sandbox/templates/react-background/background.tsx new file mode 100644 index 00000000..daa45e6a --- /dev/null +++ b/packages/sandbox/templates/react-background/background.tsx @@ -0,0 +1,15 @@ +import '@videojs/react/background/skin.css'; +import { BackgroundVideo, BackgroundVideoProvider, BackgroundVideoSkin } from '@videojs/react/background'; +import { createRoot } from 'react-dom/client'; + +export function App() { + return ( + + + + + + ); +} + +createRoot(document.getElementById('root')!).render(); diff --git a/packages/sandbox/templates/react-background/index.html b/packages/sandbox/templates/react-background/index.html new file mode 100644 index 00000000..93884a53 --- /dev/null +++ b/packages/sandbox/templates/react-background/index.html @@ -0,0 +1,13 @@ + + + + + + Sandbox — React Background + + + +
+ + + diff --git a/packages/sandbox/vite.config.ts b/packages/sandbox/vite.config.ts index dad9fc00..415471a1 100644 --- a/packages/sandbox/vite.config.ts +++ b/packages/sandbox/vite.config.ts @@ -20,7 +20,9 @@ export default defineConfig({ main: resolve(__dirname, 'src/index.html'), core: resolve(__dirname, 'src/core/index.html'), html: resolve(__dirname, 'src/html/index.html'), + 'html-background': resolve(__dirname, 'src/html-background/index.html'), react: resolve(__dirname, 'src/react/index.html'), + 'react-background': resolve(__dirname, 'src/react-background/index.html'), 'react-tailwind': resolve(__dirname, 'src/react-tailwind/index.html'), }, }, diff --git a/packages/utils/src/dom/attributes.ts b/packages/utils/src/dom/attributes.ts new file mode 100644 index 00000000..1de7b262 --- /dev/null +++ b/packages/utils/src/dom/attributes.ts @@ -0,0 +1,7 @@ +export function namedNodeMapToObject(namedNodeMap: NamedNodeMap) { + const obj: Record = {}; + for (const attr of namedNodeMap) { + obj[attr.name] = attr.value; + } + return obj; +} diff --git a/packages/utils/src/dom/index.ts b/packages/utils/src/dom/index.ts index 02c6082d..0e33a7fc 100644 --- a/packages/utils/src/dom/index.ts +++ b/packages/utils/src/dom/index.ts @@ -1,4 +1,5 @@ export { animationFrame } from './animation-frame'; +export { namedNodeMapToObject } from './attributes'; export { isRTL } from './direction'; export { type OnEventOptions, onEvent } from './event'; export { idleCallback } from './idle-callback';