feat(media)!: add Google Cast by default to HLS, DASH media (#1661)

This commit is contained in:
Wesley Luyten
2026-06-12 17:34:14 -07:00
committed by GitHub
parent 1145f09e52
commit 0ffe1a9197
64 changed files with 2684 additions and 1300 deletions
@@ -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);
+5 -1
View File
@@ -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);
+12 -6
View File
@@ -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}>
+12 -6
View File
@@ -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();
});
});
+11 -2
View File
@@ -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))
);