refactor(react): consolidate Video component into single module

- Merge connected, connected-with-defaults, and media-elements Video components
- Create unified Video.tsx in main components directory alongside PlayButton/MuteButton
- Preserve all existing functionality: media store connection, custom components, defaults
- Maintain backwards compatibility with both Video and MediaElementVideo exports
- Remove duplicate component directories and reduce architectural complexity
- Simplify component structure while keeping TypeScript types and 'use client' directive

This consolidation reduces the Video component from 3 separate files across 3 directories
to a single, comprehensive module that's easier to understand and maintain.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2025-09-08 17:23:37 -07:00
committed by Christian Pillsbury
co-authored by Claude
parent e1d326ffaf
commit a1de5ebae3
5 changed files with 126 additions and 231 deletions
@@ -0,0 +1,125 @@
'use client';
import * as React from 'react';
import { useMediaRef } from '@vjs-10/react-media-store';
import { createMediaStateOwner } from '@vjs-10/media';
import {
ElementType,
VideoHTMLAttributes,
DetailedHTMLProps,
PropsWithChildren,
CSSProperties,
Ref,
useImperativeHandle,
useRef,
} from 'react';
export type MuxVideoProps = {
'playback-id'?: string;
};
type MediaStateOwner = NonNullable<Parameters<ReturnType<typeof useMediaRef>>[0]>;
/** @TODO Improve type inference and narrowing/widening for different use cases (CJP) */
type ComponentType = ElementType<
Omit<
DetailedHTMLProps<VideoHTMLAttributes<HTMLVideoElement>, HTMLVideoElement>,
'ref'
> & { ref: Ref<MediaStateOwner> }
>;
// These are the first steps/WIP POC of decoupling the Media State Owner from the DOM.
// Note that everything will still work if you use:
// 1. an audio/video element directly
// 2. a custom element a la media-elements
type CreateMediaStateOwner = typeof createMediaStateOwner;
const useMediaStateOwner = (
ref: Ref<any>,
createMediaStateOwner: CreateMediaStateOwner /*, props? */
) => {
const mediaStateOwnerRef = useRef(createMediaStateOwner(/* props? */));
useImperativeHandle(ref, () => mediaStateOwnerRef.current, []);
/** @TODO Parameterize this (CJP) */
type ComponentProps = DetailedHTMLProps<
VideoHTMLAttributes<HTMLVideoElement>,
HTMLVideoElement
>;
return {
updateMediaElement(
mediaEl: HTMLMediaElement | null,
props: ComponentProps
) {
// NOTE: The details here will almost definitely change for a less "bare bones"/"POC" implementation of Media State Owner impl. (CJP)
mediaStateOwnerRef.current.mediaElement = mediaEl ?? undefined;
mediaStateOwnerRef.current.src = props.src as string;
if (props.muted) {
mediaStateOwnerRef.current.muted = props.muted;
}
},
};
};
const DefaultVideoComponent: ElementType<
Omit<
DetailedHTMLProps<VideoHTMLAttributes<HTMLVideoElement>, HTMLVideoElement>,
'ref'
> & { ref: Ref<any> }
> = ({ children, ref, ...props }) => {
const { updateMediaElement } = useMediaStateOwner(ref, createMediaStateOwner);
return (
<video {...props} ref={(mediaEl) => {
/** @TODO In later iterations/non-POC, we should be able to have a function that can be used directly for the `ref` prop (CJP) */
updateMediaElement(mediaEl, props);
}}>
{children}
</video>
);
};
/**
* @description This is a "thin wrapper" around the media component whose primary responsibility is to wire up the element
* to the <MediaProvider/>'s MediaStore for the media state.
* @param props - Identical to both a <video/>'s props and the <Player/> props, with one addition that may be familiar to
* MUI users: a `component` prop that allows you to use something other than the <video/> element under the hood.
* @returns A media react component (e.g. <video/>), wired up as the media element.
*/
const ConnectedVideo = ({
component,
children,
...props
}: PropsWithChildren<{
component: ComponentType;
className?: string | undefined;
style?: CSSProperties | undefined;
}>) => {
const Component = component;
const mediaRefCallback = useMediaRef();
// NOTE: While this may feel like magic to folks, in the "default" use case, you can think of it as:
// return (<video ref={mediaRefCallback} {...restProps} >{children}</video>);
return (
<Component {...props} ref={mediaRefCallback}>
{children}
</Component>
);
};
// Main Video component with default component
export const Video = ({
component = DefaultVideoComponent,
children,
...props
}: PropsWithChildren<{
component?: ComponentType;
className?: string | undefined;
style?: CSSProperties | undefined;
}>) => {
return (
<ConnectedVideo {...props} component={component}>
{children}
</ConnectedVideo>
);
};
// MediaElementVideo export (same as Video but for backwards compatibility)
export const MediaElementVideo = Video;
export default Video;
@@ -1,87 +0,0 @@
// NOTE: This is just a thin wrapper around the "skeletal" connected component that also applies a default (but overridable) definition of the
// "BaseComponent" that defines the actual UI.
// NOTE: Definitions like this should be able to be autogenerated via codegen, defined via a factory function (HoC or higher order component), or both.
// import BaseComponent from '../ui/PlayerUI';
import * as React from 'react';
import {
DetailedHTMLProps,
ElementType,
Ref,
useImperativeHandle,
useRef,
VideoHTMLAttributes,
} from 'react';
/** @TODO Improve types crud (CJP) */
import ConnectedComponent from '../connected/Video';
import { createMediaStateOwner } from '@vjs-10/media';
// These are the first steps/WIP POC of decoupling the Media State Owner from the DOM.
// Note that everything will still work if you use:
// 1. an audio/video element directly
// 2. a custom element a la media-elements
type CreateMediaStateOwner = typeof createMediaStateOwner;
const useMediaStateOwner = (
ref: Ref<any>,
createMediaStateOwner: CreateMediaStateOwner /*, props? */
) => {
const mediaStateOwnerRef = useRef(createMediaStateOwner(/* props? */));
useImperativeHandle(ref, () => mediaStateOwnerRef.current, []);
/** @TODO Parameterize this (CJP) */
type ComponentProps = DetailedHTMLProps<
VideoHTMLAttributes<HTMLVideoElement>,
HTMLVideoElement
>;
return {
updateMediaElement(
mediaEl: HTMLMediaElement | null,
props: ComponentProps
) {
// NOTE: The details here will almost definitely change for a less "bare bones"/"POC" implementation of Media State Owner impl. (CJP)
mediaStateOwnerRef.current.mediaElement = mediaEl ?? undefined;
mediaStateOwnerRef.current.src = props.src as string;
if (props.muted) {
mediaStateOwnerRef.current.muted = props.muted;
}
},
};
};
const BaseComponent: ElementType<
Omit<
DetailedHTMLProps<VideoHTMLAttributes<HTMLVideoElement>, HTMLVideoElement>,
'ref'
> & { ref: Ref<any> }
> = ({ children, ref, ...props }) => {
const { updateMediaElement } = useMediaStateOwner(ref, createMediaStateOwner);
return (
<video {...props} ref={(mediaEl) => {
/** @TODO In later iterations/non-POC, we should be able to have a function that can be used directly for the `ref` prop (CJP) */
updateMediaElement(mediaEl, props);
}}>
{children}
</video>
);
};
// NOTE: Assuming FC or "functional component" here for type expediency (though may be fine for our use cases).
type ConnectedComponentProps = Parameters<typeof ConnectedComponent>[0];
type ConnectedComponentReturnType = ReturnType<typeof ConnectedComponent>;
type DefaultedProps = 'component';
type ConnectedComponentWithDefaults = (
props: Omit<ConnectedComponentProps, DefaultedProps> &
Partial<Pick<ConnectedComponentProps, DefaultedProps>>,
) => ConnectedComponentReturnType;
const Component: ConnectedComponentWithDefaults = ({
component = BaseComponent,
children,
...props
}) => {
return (
<ConnectedComponent {...props} component={component}>
{children}
</ConnectedComponent>
);
};
export default Component;
@@ -1,54 +0,0 @@
'use client';
import * as React from 'react';
import { useMediaRef } from '@vjs-10/react-media-store';
import {
ElementType,
VideoHTMLAttributes,
DetailedHTMLProps,
PropsWithChildren,
CSSProperties,
Ref,
} from 'react';
export type MuxVideoProps = {
'playback-id'?: string;
};
type MediaStateOwner = NonNullable<Parameters<ReturnType<typeof useMediaRef>>[0]>;
/** @TODO Improve type inference and narrowing/widening for different use cases (CJP) */
type ComponentType = ElementType<
Omit<
DetailedHTMLProps<VideoHTMLAttributes<HTMLVideoElement>, HTMLVideoElement>,
'ref'
> & { ref: Ref<MediaStateOwner> }
>;
/**
* @description This is a "thin wrapper" around the media component whose primary responsibility is to wire up the element
* to the <MediaProvider/>'s MediaStore for the media state.
* @param props - Identical to both a <video/>'s props and the <Player/> props, with one addition that may be familiar to
* MUI users: a `component` prop that allows you to use something other than the <video/> element under the hood.
* @returns A media react component (e.g. <video/>), wired up as the media element.
*/
const Video = ({
component,
children,
...props
}: PropsWithChildren<{
component: ComponentType;
className?: string | undefined;
style?: CSSProperties | undefined;
}>) => {
const Component = component;
const mediaRefCallback = useMediaRef();
// NOTE: While this may feel like magic to folks, in the "default" use case, you can think of it as:
// return (<video ref={mediaRefCallback} {...restProps} >{children}</video>);
return (
<Component {...props} ref={mediaRefCallback}>
{children}
</Component>
);
};
export default Video;
@@ -1,87 +0,0 @@
// NOTE: This is just a thin wrapper around the "skeletal" connected component that also applies a default (but overridable) definition of the
// "BaseComponent" that defines the actual UI.
// NOTE: Definitions like this should be able to be autogenerated via codegen, defined via a factory function (HoC or higher order component), or both.
// import BaseComponent from '../ui/PlayerUI';
import * as React from 'react';
import {
DetailedHTMLProps,
ElementType,
Ref,
useImperativeHandle,
useRef,
VideoHTMLAttributes,
} from 'react';
/** @TODO Improve types crud (CJP) */
import ConnectedComponent from '../connected/Video';
import { createMediaStateOwner } from '@vjs-10/media';
// These are the first steps/WIP POC of decoupling the Media State Owner from the DOM.
// Note that everything will still work if you use:
// 1. an audio/video element directly
// 2. a custom element a la media-elements
type CreateMediaStateOwner = typeof createMediaStateOwner;
const useMediaStateOwner = (
ref: Ref<any>,
createMediaStateOwner: CreateMediaStateOwner /*, props? */
) => {
const mediaStateOwnerRef = useRef(createMediaStateOwner(/* props? */));
useImperativeHandle(ref, () => mediaStateOwnerRef.current, []);
/** @TODO Parameterize this (CJP) */
type ComponentProps = DetailedHTMLProps<
VideoHTMLAttributes<HTMLVideoElement>,
HTMLVideoElement
>;
return {
updateMediaElement(
mediaEl: HTMLMediaElement | null,
props: ComponentProps
) {
// NOTE: The details here will almost definitely change for a less "bare bones"/"POC" implementation of Media State Owner impl. (CJP)
mediaStateOwnerRef.current.mediaElement = mediaEl ?? undefined;
mediaStateOwnerRef.current.src = props.src as string;
if (props.muted) {
mediaStateOwnerRef.current.muted = props.muted;
}
},
};
};
const BaseComponent: ElementType<
Omit<
DetailedHTMLProps<VideoHTMLAttributes<HTMLVideoElement>, HTMLVideoElement>,
'ref'
> & { ref: Ref<any> }
> = ({ children, ref, ...props }) => {
const { updateMediaElement } = useMediaStateOwner(ref, createMediaStateOwner);
return (
<video {...props} ref={(mediaEl) => {
/** @TODO In later iterations/non-POC, we should be able to have a function that can be used directly for the `ref` prop (CJP) */
updateMediaElement(mediaEl, props);
}}>
{children}
</video>
);
};
// NOTE: Assuming FC or "functional component" here for type expediency (though may be fine for our use cases).
type ConnectedComponentProps = Parameters<typeof ConnectedComponent>[0];
type ConnectedComponentReturnType = ReturnType<typeof ConnectedComponent>;
type DefaultedProps = 'component';
type ConnectedComponentWithDefaults = (
props: Omit<ConnectedComponentProps, DefaultedProps> &
Partial<Pick<ConnectedComponentProps, DefaultedProps>>,
) => ConnectedComponentReturnType;
const Component: ConnectedComponentWithDefaults = ({
component = BaseComponent,
children,
...props
}) => {
return (
<ConnectedComponent {...props} component={component}>
{children}
</ConnectedComponent>
);
};
export default Component;
+1 -3
View File
@@ -2,6 +2,4 @@
export * from '@vjs-10/react-media-store';
export * from './skins/MediaSkinDefault';
import Video from './components/connected-with-defaults/Video';
import MediaElementVideo from './components/media-elements/Video';
export { Video, MediaElementVideo };
export { Video, MediaElementVideo } from './components/Video';