mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(packages): add poster placeholder blur-up pattern (#1632)
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
||||
} from '@videojs/skins/minimal/tailwind/video.tailwind';
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { type ComponentProps, type CSSProperties, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
AirPlayEnterIcon,
|
||||
AirPlayExitIcon,
|
||||
@@ -196,10 +196,14 @@ function CaptionsTrigger(): ReactNode {
|
||||
}
|
||||
|
||||
export function MinimalLiveVideoSkinTailwind(props: MinimalLiveVideoSkinProps): ReactNode {
|
||||
const { children, className, poster: posterProp, ...rest } = props;
|
||||
const { children, className, poster: posterProp, placeholder, style, ...rest } = props;
|
||||
|
||||
const containerStyle = placeholder
|
||||
? ({ '--media-poster-placeholder': `url(${placeholder})`, ...style } as CSSProperties)
|
||||
: style;
|
||||
|
||||
return (
|
||||
<Container className={cn(root(false), className)} {...rest}>
|
||||
<Container className={cn(root(false), className)} style={containerStyle} {...rest}>
|
||||
{children}
|
||||
|
||||
{posterProp && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { type ComponentProps, type CSSProperties, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
AirPlayEnterIcon,
|
||||
AirPlayExitIcon,
|
||||
@@ -165,10 +165,18 @@ function CaptionsTrigger(): ReactNode {
|
||||
}
|
||||
|
||||
export function MinimalLiveVideoSkin(props: MinimalLiveVideoSkinProps): ReactNode {
|
||||
const { children, className, poster, ...rest } = props;
|
||||
const { children, className, poster, placeholder, style, ...rest } = props;
|
||||
|
||||
const containerStyle = placeholder
|
||||
? ({ '--media-poster-placeholder': `url(${placeholder})`, ...style } as CSSProperties)
|
||||
: style;
|
||||
|
||||
return (
|
||||
<Container className={cn('media-minimal-skin media-minimal-skin--video', className)} {...rest}>
|
||||
<Container
|
||||
className={cn('media-minimal-skin media-minimal-skin--video', className)}
|
||||
style={containerStyle}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
||||
{poster && (
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from '@videojs/skins/default/tailwind/video.tailwind';
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { type ComponentProps, type CSSProperties, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
AirPlayEnterIcon,
|
||||
AirPlayExitIcon,
|
||||
@@ -196,10 +196,14 @@ function CaptionsTrigger(): ReactNode {
|
||||
}
|
||||
|
||||
export function LiveVideoSkinTailwind(props: LiveVideoSkinProps): ReactNode {
|
||||
const { children, className, poster: posterProp, ...rest } = props;
|
||||
const { children, className, poster: posterProp, placeholder, style, ...rest } = props;
|
||||
|
||||
const containerStyle = placeholder
|
||||
? ({ '--media-poster-placeholder': `url(${placeholder})`, ...style } as CSSProperties)
|
||||
: style;
|
||||
|
||||
return (
|
||||
<Container className={cn(root(false), className)} {...rest}>
|
||||
<Container className={cn(root(false), className)} style={containerStyle} {...rest}>
|
||||
{children}
|
||||
|
||||
{posterProp && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { type ComponentProps, type CSSProperties, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
AirPlayEnterIcon,
|
||||
AirPlayExitIcon,
|
||||
@@ -157,10 +157,18 @@ function CaptionsTrigger(): ReactNode {
|
||||
}
|
||||
|
||||
export function LiveVideoSkin(props: LiveVideoSkinProps): ReactNode {
|
||||
const { children, className, poster, ...rest } = props;
|
||||
const { children, className, poster, placeholder, style, ...rest } = props;
|
||||
|
||||
const containerStyle = placeholder
|
||||
? ({ '--media-poster-placeholder': `url(${placeholder})`, ...style } as CSSProperties)
|
||||
: style;
|
||||
|
||||
return (
|
||||
<Container className={cn('media-default-skin media-default-skin--video', className)} {...rest}>
|
||||
<Container
|
||||
className={cn('media-default-skin media-default-skin--video', className)}
|
||||
style={containerStyle}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
||||
{poster && (
|
||||
|
||||
@@ -11,4 +11,6 @@ export type BaseSkinProps<T = unknown> = PropsWithChildren<
|
||||
|
||||
export type BaseVideoSkinProps<T = unknown> = BaseSkinProps<T> & {
|
||||
poster?: string | RenderProp<Poster.State> | undefined;
|
||||
/** Low-resolution placeholder shown behind the poster while it loads (blur-up effect). */
|
||||
placeholder?: string | undefined;
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from '@videojs/skins/minimal/tailwind/video.tailwind';
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { type ComponentProps, type CSSProperties, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
AirPlayEnterIcon,
|
||||
AirPlayExitIcon,
|
||||
@@ -346,10 +346,14 @@ function SettingsMenu(): ReactNode {
|
||||
/* ------------------------------------------ Skin ------------------------------------------- */
|
||||
|
||||
export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNode {
|
||||
const { children, className, poster: posterProp, ...rest } = props;
|
||||
const { children, className, poster: posterProp, placeholder, style, ...rest } = props;
|
||||
|
||||
const containerStyle = placeholder
|
||||
? ({ '--media-poster-placeholder': `url(${placeholder})`, ...style } as CSSProperties)
|
||||
: style;
|
||||
|
||||
return (
|
||||
<Container className={cn(root(false), className)} {...rest}>
|
||||
<Container className={cn(root(false), className)} style={containerStyle} {...rest}>
|
||||
{children}
|
||||
|
||||
{posterProp && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { type ComponentProps, type CSSProperties, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
AirPlayEnterIcon,
|
||||
AirPlayExitIcon,
|
||||
@@ -283,10 +283,18 @@ function SettingsMenu(): ReactNode {
|
||||
}
|
||||
|
||||
export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
|
||||
const { children, className, poster, ...rest } = props;
|
||||
const { children, className, poster, placeholder, style, ...rest } = props;
|
||||
|
||||
const containerStyle = placeholder
|
||||
? ({ '--media-poster-placeholder': `url(${placeholder})`, ...style } as CSSProperties)
|
||||
: style;
|
||||
|
||||
return (
|
||||
<Container className={cn('media-minimal-skin media-minimal-skin--video', className)} {...rest}>
|
||||
<Container
|
||||
className={cn('media-minimal-skin media-minimal-skin--video', className)}
|
||||
style={containerStyle}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
||||
{poster && (
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from '@videojs/skins/default/tailwind/video.tailwind';
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { type ComponentProps, type CSSProperties, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
AirPlayEnterIcon,
|
||||
AirPlayExitIcon,
|
||||
@@ -346,10 +346,14 @@ function SettingsMenu(): ReactNode {
|
||||
/* ------------------------------------------ Skin ------------------------------------------- */
|
||||
|
||||
export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
|
||||
const { children, className, poster: posterProp, ...rest } = props;
|
||||
const { children, className, poster: posterProp, placeholder, style, ...rest } = props;
|
||||
|
||||
const containerStyle = placeholder
|
||||
? ({ '--media-poster-placeholder': `url(${placeholder})`, ...style } as CSSProperties)
|
||||
: style;
|
||||
|
||||
return (
|
||||
<Container className={cn(root(false), className)} {...rest}>
|
||||
<Container className={cn(root(false), className)} style={containerStyle} {...rest}>
|
||||
{children}
|
||||
|
||||
{posterProp && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { type ComponentProps, type CSSProperties, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
AirPlayEnterIcon,
|
||||
AirPlayExitIcon,
|
||||
@@ -283,10 +283,18 @@ function SettingsMenu(): ReactNode {
|
||||
}
|
||||
|
||||
export function VideoSkin(props: VideoSkinProps): ReactNode {
|
||||
const { children, className, poster, ...rest } = props;
|
||||
const { children, className, poster, placeholder, style, ...rest } = props;
|
||||
|
||||
const containerStyle = placeholder
|
||||
? ({ '--media-poster-placeholder': `url(${placeholder})`, ...style } as CSSProperties)
|
||||
: style;
|
||||
|
||||
return (
|
||||
<Container className={cn('media-default-skin media-default-skin--video', className)} {...rest}>
|
||||
<Container
|
||||
className={cn('media-default-skin media-default-skin--video', className)}
|
||||
style={containerStyle}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
||||
{poster && (
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { PosterCore, PosterDataAttrs } from '@videojs/core';
|
||||
import { logMissingFeature, selectPlayback } from '@videojs/core/dom';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef, useState } from 'react';
|
||||
import type { ForwardedRef, SyntheticEvent } from 'react';
|
||||
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { usePlayer } from '../../player/context';
|
||||
import type { UIComponentProps } from '../../utils/types';
|
||||
@@ -35,6 +35,26 @@ export const Poster = forwardRef(function Poster(
|
||||
|
||||
const [core] = useState(() => new PosterCore());
|
||||
|
||||
// Track when the current src has finished loading so the CSS blur-up
|
||||
// sequence can show the placeholder first, then crossfade to the full image.
|
||||
const src = (elementProps as { src?: string }).src;
|
||||
const [loadedSrc, setLoadedSrc] = useState<string | undefined>(undefined);
|
||||
const loaded = loadedSrc === src;
|
||||
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||
|
||||
// A cached image may already be complete when the element mounts, in which
|
||||
// case onLoad never fires. Check synchronously after mount and on src change.
|
||||
useEffect(() => {
|
||||
const img = imgRef.current;
|
||||
if (img?.complete && img.naturalWidth > 0 && img.getAttribute('src') === src) {
|
||||
setLoadedSrc(src);
|
||||
}
|
||||
}, [src]);
|
||||
|
||||
const handleLoad = useCallback((event: SyntheticEvent<HTMLImageElement>) => {
|
||||
setLoadedSrc(event.currentTarget.getAttribute('src') ?? undefined);
|
||||
}, []);
|
||||
|
||||
if (!playback) {
|
||||
if (__DEV__) logMissingFeature('Poster', 'playback');
|
||||
return null;
|
||||
@@ -48,8 +68,8 @@ export const Poster = forwardRef(function Poster(
|
||||
{
|
||||
state: core.getState(),
|
||||
stateAttrMap: PosterDataAttrs,
|
||||
ref: [forwardedRef],
|
||||
props: [elementProps],
|
||||
ref: [forwardedRef, imgRef],
|
||||
props: [elementProps, { 'data-loaded': loaded ? '' : undefined, onLoad: handleLoad }],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user