mirror of
https://github.com/zoriya/v10.git
synced 2026-08-05 05:37:21 +00:00
feat(media)!: add Google Cast by default to HLS, DASH media (#1661)
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
import type { DashMediaProps } from '@videojs/core/dom/media/dash';
|
||||
import { DashMedia, dashMediaDefaultProps } from '@videojs/core/dom/media/dash';
|
||||
import { GoogleCast } from '@videojs/core/dom/media/google-cast';
|
||||
import { addComponent } from '@videojs/core/dom/media/media-host';
|
||||
import type { ReactNode, VideoHTMLAttributes } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { useAttachMedia } from '../../utils/use-attach-media';
|
||||
@@ -16,7 +18,9 @@ export interface DashVideoProps
|
||||
}
|
||||
|
||||
export const DashVideo = forwardRef<HTMLVideoElement, DashVideoProps>(function DashVideo({ children, ...props }, ref) {
|
||||
const media = useMediaInstance(DashMedia);
|
||||
const media = useMediaInstance(DashMedia, (media) => {
|
||||
addComponent(media, new GoogleCast());
|
||||
});
|
||||
const attachRef = useAttachMedia(media);
|
||||
const composedRef = useComposedRefs(attachRef, ref);
|
||||
const htmlProps = useSyncProps(media, props, dashMediaDefaultProps);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { GoogleCast } from '@videojs/core/dom/media/google-cast';
|
||||
import type { HlsMediaProps } from '@videojs/core/dom/media/hls';
|
||||
import { HlsMedia, hlsMediaDefaultProps } from '@videojs/core/dom/media/hls';
|
||||
import { addComponent } from '@videojs/core/dom/media/media-host';
|
||||
import type { ReactNode, VideoHTMLAttributes } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { useAttachMedia } from '../../utils/use-attach-media';
|
||||
@@ -16,7 +18,9 @@ export interface HlsVideoProps
|
||||
}
|
||||
|
||||
export const HlsVideo = forwardRef<HTMLVideoElement, HlsVideoProps>(function HlsVideo({ children, ...props }, ref) {
|
||||
const media = useMediaInstance(HlsMedia);
|
||||
const media = useMediaInstance(HlsMedia, (media) => {
|
||||
addComponent(media, new GoogleCast());
|
||||
});
|
||||
const attachRef = useAttachMedia(media);
|
||||
const composedRef = useComposedRefs(attachRef, ref);
|
||||
const htmlProps = useSyncProps(media, props, hlsMediaDefaultProps);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import type { MuxMediaProps } from '@videojs/core/dom/media/mux';
|
||||
import { MuxAudioMedia, muxMediaDefaultProps } from '@videojs/core/dom/media/mux';
|
||||
import { GoogleCast } from '@videojs/core/dom/media/google-cast';
|
||||
import type { HlsMediaProps } from '@videojs/core/dom/media/hls';
|
||||
import { HlsMedia, hlsMediaDefaultProps } from '@videojs/core/dom/media/hls';
|
||||
import { addComponent } from '@videojs/core/dom/media/media-host';
|
||||
import { MuxData } from '@videojs/core/dom/media/mux';
|
||||
import type { AudioHTMLAttributes, ReactNode } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { useAttachMedia } from '../../utils/use-attach-media';
|
||||
@@ -10,16 +13,19 @@ import { useMediaInstance } from '../../utils/use-media-instance';
|
||||
import { useSyncProps } from '../../utils/use-sync-props';
|
||||
|
||||
export interface MuxAudioProps
|
||||
extends Omit<AudioHTMLAttributes<HTMLAudioElement>, keyof MuxMediaProps>,
|
||||
Partial<MuxMediaProps> {
|
||||
extends Omit<AudioHTMLAttributes<HTMLAudioElement>, keyof HlsMediaProps>,
|
||||
Partial<HlsMediaProps> {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const MuxAudio = forwardRef<HTMLAudioElement, MuxAudioProps>(function MuxAudio({ children, ...props }, ref) {
|
||||
const media = useMediaInstance(MuxAudioMedia);
|
||||
const media = useMediaInstance(HlsMedia, (media) => {
|
||||
addComponent(media, new MuxData({ playerSoftwareName: 'mux-audio' }));
|
||||
addComponent(media, new GoogleCast());
|
||||
});
|
||||
const attachRef = useAttachMedia(media);
|
||||
const composedRef = useComposedRefs(attachRef, ref);
|
||||
const htmlProps = useSyncProps(media, props, muxMediaDefaultProps);
|
||||
const htmlProps = useSyncProps(media, props, hlsMediaDefaultProps);
|
||||
|
||||
return (
|
||||
<audio ref={composedRef} {...htmlProps}>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import type { MuxMediaProps } from '@videojs/core/dom/media/mux';
|
||||
import { MuxVideoMedia, muxMediaDefaultProps } from '@videojs/core/dom/media/mux';
|
||||
import { GoogleCast } from '@videojs/core/dom/media/google-cast';
|
||||
import type { HlsMediaProps } from '@videojs/core/dom/media/hls';
|
||||
import { HlsMedia, hlsMediaDefaultProps } from '@videojs/core/dom/media/hls';
|
||||
import { addComponent } from '@videojs/core/dom/media/media-host';
|
||||
import { MuxData } from '@videojs/core/dom/media/mux';
|
||||
import type { ReactNode, VideoHTMLAttributes } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { useAttachMedia } from '../../utils/use-attach-media';
|
||||
@@ -10,16 +13,19 @@ import { useMediaInstance } from '../../utils/use-media-instance';
|
||||
import { useSyncProps } from '../../utils/use-sync-props';
|
||||
|
||||
export interface MuxVideoProps
|
||||
extends Omit<VideoHTMLAttributes<HTMLVideoElement>, keyof MuxMediaProps>,
|
||||
Partial<MuxMediaProps> {
|
||||
extends Omit<VideoHTMLAttributes<HTMLVideoElement>, keyof HlsMediaProps>,
|
||||
Partial<HlsMediaProps> {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const MuxVideo = forwardRef<HTMLVideoElement, MuxVideoProps>(function MuxVideo({ children, ...props }, ref) {
|
||||
const media = useMediaInstance(MuxVideoMedia);
|
||||
const media = useMediaInstance(HlsMedia, (media) => {
|
||||
addComponent(media, new MuxData({ playerSoftwareName: 'mux-video' }));
|
||||
addComponent(media, new GoogleCast());
|
||||
});
|
||||
const attachRef = useAttachMedia(media);
|
||||
const composedRef = useComposedRefs(attachRef, ref);
|
||||
const htmlProps = useSyncProps(media, props, muxMediaDefaultProps);
|
||||
const htmlProps = useSyncProps(media, props, hlsMediaDefaultProps);
|
||||
|
||||
return (
|
||||
<video ref={composedRef} {...htmlProps}>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { GoogleCast } from '@videojs/core/dom/media/google-cast';
|
||||
import { addComponent } from '@videojs/core/dom/media/media-host';
|
||||
import type { NativeHlsMediaProps } from '@videojs/core/dom/media/native-hls';
|
||||
import { NativeHlsMedia, nativeHlsMediaDefaultProps } from '@videojs/core/dom/media/native-hls';
|
||||
import type { ReactNode, VideoHTMLAttributes } from 'react';
|
||||
@@ -19,7 +21,9 @@ export const NativeHlsVideo = forwardRef<HTMLVideoElement, NativeHlsVideoProps>(
|
||||
{ children, ...props },
|
||||
ref
|
||||
) {
|
||||
const media = useMediaInstance(NativeHlsMedia);
|
||||
const media = useMediaInstance(NativeHlsMedia, (media) => {
|
||||
addComponent(media, new GoogleCast());
|
||||
});
|
||||
const attachRef = useAttachMedia(media);
|
||||
const composedRef = useComposedRefs(attachRef, ref);
|
||||
const htmlProps = useSyncProps(media, props, nativeHlsMediaDefaultProps);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { MuxData } from '@videojs/core/dom/media/mux';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { MuxVideo } from '../mux-video';
|
||||
|
||||
describe('MuxVideo', () => {
|
||||
it('routes component config to the MuxData component', () => {
|
||||
const envKey = vi.spyOn(MuxData.prototype, 'envKey', 'set');
|
||||
|
||||
// `useSyncProps` writes `media.config` during render, before the mount
|
||||
// effect registers the components — `addComponent` adopts the early value.
|
||||
const { container } = render(<MuxVideo config={{ muxData: { envKey: 'test-key' } }} />);
|
||||
|
||||
expect(envKey).toHaveBeenCalledWith('test-key');
|
||||
// The config prop is consumed by the media, not spread onto the element.
|
||||
expect(container.querySelector('video')!.hasAttribute('config')).toBe(false);
|
||||
|
||||
envKey.mockRestore();
|
||||
});
|
||||
|
||||
it('does not reinitialize mux data when the same config is re-rendered', () => {
|
||||
const reinit = vi.spyOn(MuxData.prototype, 'envKey', 'set');
|
||||
|
||||
const { rerender } = render(<MuxVideo config={{ muxData: { envKey: 'test-key' } }} />);
|
||||
rerender(<MuxVideo config={{ muxData: { envKey: 'test-key' } }} />);
|
||||
|
||||
// The setter runs per assignment but dedupes same values internally;
|
||||
// assert it was only handed the same value.
|
||||
expect(reinit).toHaveBeenCalledWith('test-key');
|
||||
expect(reinit.mock.calls.every(([value]) => value === 'test-key')).toBe(true);
|
||||
|
||||
reinit.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -12,16 +12,25 @@ import { useDestroy } from './use-destroy';
|
||||
* Instantiates the media class once, attaches it to the player on mount,
|
||||
* and safely detaches on unmount using a functional updater to avoid race
|
||||
* conditions when swapping media components (e.g. DashVideo → HlsVideo).
|
||||
*
|
||||
* An optional `setup` callback runs once on mount — e.g. to add media
|
||||
* components via `addComponent`. Components registered there are destroyed
|
||||
* together with the media instance on unmount (`media.destroy()` destroys
|
||||
* all of its registered components).
|
||||
*/
|
||||
export function useMediaInstance<Instance extends Media & { destroy(): void }>(
|
||||
MediaClass: new () => Instance
|
||||
MediaClass: new () => Instance,
|
||||
setup?: (media: Instance) => void
|
||||
): Instance {
|
||||
const [instance] = useState(() => new MediaClass());
|
||||
const setMedia = useMediaAttach();
|
||||
|
||||
useDestroy(
|
||||
instance,
|
||||
() => setMedia?.(instance),
|
||||
() => {
|
||||
setup?.(instance);
|
||||
setMedia?.(instance);
|
||||
},
|
||||
() => setMedia?.((prev) => (prev === instance ? null : prev))
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user