Skip to content

Commit

Permalink
feat(player): google cast wip
Browse files Browse the repository at this point in the history
  • Loading branch information
mihar-22 committed Jan 5, 2024
1 parent f49044f commit c46c577
Show file tree
Hide file tree
Showing 92 changed files with 3,165 additions and 445 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ packages/vidstack/player/styles/default/theme.css
packages/react/src/icons.ts
packages/react/index.d.ts
packages/react/icons.d.ts
packages/react/dom.d.ts
packages/react/google-cast.d.ts
packages/react/player/
packages/react/tailwind*

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"clean": "turbo run clean",
"format": "turbo run format --parallel",
"test": "turbo run test --parallel",
"preinstall": "npx only-allow pnpm && pnpx simple-git-hooks",
"preinstall": "npx only-allow pnpm",
"release": "pnpm build && node .scripts/release.js --next",
"release:dry": "pnpm run release --dry",
"validate": "turbo run build format test"
Expand Down
2 changes: 2 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
},
"./player/styles/*": "./player/styles/*",
"./dist/types/*": "./dist/types/*",
"./dom.d.ts": "./dom.d.ts",
"./google-cast.d.ts": "./google-cast.d.ts",
"./package.json": "./package.json",
"./tailwind.cjs": {
"types": "./tailwind.d.cts",
Expand Down
78 changes: 50 additions & 28 deletions packages/react/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const EXTERNAL_PACKAGES = [
/^remotion/,
],
NPM_BUNDLES = [define({ dev: true }), define({ dev: false })],
TYPES_BUNDLES = [defineTypes()];
TYPES_BUNDLES = defineTypes();

// Styles
if (!MODE_TYPES) {
Expand All @@ -53,38 +53,60 @@ export default defineConfig(
);

/**
* @returns {import('rollup').RollupOptions}
* @returns {import('rollup').RollupOptions[]}
* */
function defineTypes() {
return {
input: {
index: 'types/react/src/index.d.ts',
icons: 'types/react/src/icons.d.ts',
'player/remotion': 'types/react/src/providers/remotion/index.d.ts',
'player/layouts/default': 'types/react/src/components/layouts/default/index.d.ts',
},
output: {
dir: '.',
chunkFileNames: 'dist/types/[name].d.ts',
manualChunks(id) {
if (id.includes('react/src')) return 'vidstack-react';
if (id.includes('maverick')) return 'vidstack-framework';
if (id.includes('vidstack')) return 'vidstack';
return [
{
input: {
index: 'types/react/src/index.d.ts',
icons: 'types/react/src/icons.d.ts',
'player/remotion': 'types/react/src/providers/remotion/index.d.ts',
'player/layouts/default': 'types/react/src/components/layouts/default/index.d.ts',
},
},
external: EXTERNAL_PACKAGES,
plugins: [
{
name: 'resolve-vidstack-types',
resolveId(id) {
if (id === 'vidstack') {
return 'types/vidstack/src/index.d.ts';
}
output: {
dir: '.',
chunkFileNames: 'dist/types/[name].d.ts',
manualChunks(id) {
if (id.includes('react/src')) return 'vidstack-react';
if (id.includes('maverick')) return 'vidstack-framework';
if (id.includes('vidstack')) return 'vidstack';
},
},
dts({ respectExternal: true }),
],
};
external: EXTERNAL_PACKAGES,
plugins: [
{
name: 'resolve-vidstack-types',
resolveId(id) {
if (id === 'vidstack') {
return 'types/vidstack/src/index.d.ts';
}
},
},
dts({
respectExternal: true,
}),
{
name: 'globals',
generateBundle(_, bundle) {
const indexFile = Object.values(bundle).find((file) => file.fileName === 'index.d.ts'),
globalFiles = ['dom.d.ts', 'google-cast.d.ts'],
references = globalFiles
.map((path) => `/// <reference path="./${path}" />`)
.join('\n');

for (const file of globalFiles) {
fs.copyFileSync(path.resolve(`../vidstack/${file}`), file);
}

if (indexFile?.type === 'chunk') {
indexFile.code = references + `\n\n${indexFile.code}`;
}
},
},
],
},
];
}

/**
Expand Down
22 changes: 11 additions & 11 deletions packages/react/src/components/layouts/default/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@ import type { DefaultLayoutIcons } from './icons';
export const DefaultLayoutContext = React.createContext<DefaultLayoutContext>({} as any);

interface DefaultLayoutContext {
thumbnails: ThumbnailSrc;
menuContainer?: React.RefObject<HTMLElement | null>;
translations?: DefaultLayoutTranslations | null;
isSmallLayout: boolean;
showMenuDelay?: number;
showTooltipDelay: number;
disableTimeSlider: boolean;
hideQualityBitrate: boolean;
menuGroup: 'top' | 'bottom';
noModal: boolean;
Icons: DefaultLayoutIcons;
slots?: unknown;
sliderChaptersMinWidth: number;
disableTimeSlider: boolean;
isSmallLayout: boolean;
menuContainer?: React.RefObject<HTMLElement | null>;
menuGroup: 'top' | 'bottom';
noGestures: boolean;
noKeyboardActionDisplay: boolean;
noModal: boolean;
showMenuDelay?: number;
showTooltipDelay: number;
sliderChaptersMinWidth: number;
slots?: unknown;
thumbnails: ThumbnailSrc;
translations?: DefaultLayoutTranslations | null;
}

export function useDefaultLayoutLang(word: keyof DefaultLayoutTranslations) {
Expand Down
9 changes: 9 additions & 0 deletions packages/react/src/components/layouts/default/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import airPlayPaths from 'media-icons/dist/icons/airplay.js';
import arrowLeftPaths from 'media-icons/dist/icons/arrow-left.js';
import chaptersIconPaths from 'media-icons/dist/icons/chapters.js';
import arrowRightPaths from 'media-icons/dist/icons/chevron-right.js';
import googleCastPaths from 'media-icons/dist/icons/chromecast.js';
import ccOnIconPaths from 'media-icons/dist/icons/closed-captions-on.js';
import ccIconPaths from 'media-icons/dist/icons/closed-captions.js';
import exitFullscreenIconPaths from 'media-icons/dist/icons/fullscreen-exit.js';
Expand Down Expand Up @@ -38,6 +39,9 @@ export const defaultLayoutIcons: DefaultLayoutIcons = {
AirPlayButton: {
Default: createIcon(airPlayPaths),
},
GoogleCastButton: {
Default: createIcon(googleCastPaths),
},
PlayButton: {
Play: createIcon(playIconPaths),
Pause: createIcon(pauseIconPaths),
Expand Down Expand Up @@ -102,6 +106,11 @@ export interface DefaultLayoutIcons {
Connecting?: DefaultLayoutIcon;
Connected?: DefaultLayoutIcon;
};
GoogleCastButton: {
Default: DefaultLayoutIcon;
Connecting?: DefaultLayoutIcon;
Connected?: DefaultLayoutIcon;
};
PlayButton: {
Play: DefaultLayoutIcon;
Pause: DefaultLayoutIcon;
Expand Down
29 changes: 29 additions & 0 deletions packages/react/src/components/layouts/default/shared-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { PrimitivePropsWithRef } from '../../primitives/nodes';
import { AirPlayButton } from '../../ui/buttons/airplay-button';
import { CaptionButton } from '../../ui/buttons/caption-button';
import { FullscreenButton } from '../../ui/buttons/fullscreen-button';
import { GoogleCastButton } from '../../ui/buttons/google-cast-button';
import { LiveButton } from '../../ui/buttons/live-button';
import { MuteButton } from '../../ui/buttons/mute-button';
import { PIPButton } from '../../ui/buttons/pip-button';
Expand Down Expand Up @@ -296,6 +297,34 @@ function DefaultAirPlayButton({ tooltip }: DefaultMediaButtonProps) {
DefaultAirPlayButton.displayName = 'DefaultAirPlayButton';
export { DefaultAirPlayButton };

/* -------------------------------------------------------------------------------------------------
* DefaultGoogleCastButton
* -----------------------------------------------------------------------------------------------*/

function DefaultGoogleCastButton({ tooltip }: DefaultMediaButtonProps) {
const { Icons } = React.useContext(DefaultLayoutContext),
googleCastText = useDefaultLayoutLang('Google Cast'),
state = useMediaState('remotePlaybackState'),
stateText = useDefaultLayoutLang(uppercaseFirstChar(state) as Capitalize<RemotePlaybackState>),
label = `${googleCastText} ${stateText}`,
Icon =
(state === 'connecting'
? Icons.GoogleCastButton.Connecting
: state === 'connected'
? Icons.GoogleCastButton.Connected
: null) ?? Icons.GoogleCastButton.Default;
return (
<DefaultTooltip content={googleCastText} placement={tooltip}>
<GoogleCastButton className="vds-google-cast-button vds-button" aria-label={label}>
{React.createElement(Icon, { className: 'vds-icon' })}
</GoogleCastButton>
</DefaultTooltip>
);
}

DefaultGoogleCastButton.displayName = 'DefaultGoogleCastButton';
export { DefaultGoogleCastButton };

/* -------------------------------------------------------------------------------------------------
* DefaultPlayButton
* -----------------------------------------------------------------------------------------------*/
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/components/layouts/default/slots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type DefaultLayoutSlotName =
| 'muteButton'
| 'pipButton'
| 'airPlayButton'
| 'googleCastButton'
| 'playButton'
| 'loadButton'
| 'seekBackwardButton'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
DefaultChaptersMenu,
DefaultChapterTitle,
DefaultFullscreenButton,
DefaultGoogleCastButton,
DefaultMuteButton,
DefaultPIPButton,
DefaultPlayButton,
Expand Down Expand Up @@ -104,6 +105,7 @@ function DefaultVideoLargeLayout() {
{slot(slots, 'captionButton', <DefaultCaptionButton tooltip="top" />)}
{menuGroup === 'bottom' && <DefaultVideoMenus slots={slots} />}
{slot(slots, 'airPlayButton', <DefaultAirPlayButton tooltip="top" />)}
{slot(slots, 'googleCastButton', <DefaultGoogleCastButton tooltip="top" />)}
{slot(slots, 'pipButton', <DefaultPIPButton tooltip="top" />)}
{slot(slots, 'fullscreenButton', <DefaultFullscreenButton tooltip="top end" />)}
</Controls.Group>
Expand All @@ -129,6 +131,7 @@ function DefaultVideoSmallLayout() {
<Controls.Root className="vds-controls">
<Controls.Group className="vds-controls-group">
{slot(slots, 'airPlayButton', <DefaultAirPlayButton tooltip="top start" />)}
{slot(slots, 'googleCastButton', <DefaultGoogleCastButton tooltip="top start" />)}
<div className="vds-controls-spacer" />
{slot(slots, 'captionButton', <DefaultCaptionButton tooltip="bottom" />)}
<DefaultVideoMenus slots={slots} />
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/components/primitives/instances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ControlsGroup,
FullscreenButton,
Gesture,
GoogleCastButton,
LiveButton,
MediaPlayer,
MediaProvider,
Expand Down Expand Up @@ -52,6 +53,7 @@ export class MuteButtonInstance extends MuteButton {}
export class PIPButtonInstance extends PIPButton {}
export class PlayButtonInstance extends PlayButton {}
export class AirPlayButtonInstance extends AirPlayButton {}
export class GoogleCastButtonInstance extends GoogleCastButton {}
export class SeekButtonInstance extends SeekButton {}
// Tooltip
export class TooltipInstance extends Tooltip {}
Expand Down
24 changes: 13 additions & 11 deletions packages/react/src/components/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,8 @@ interface MediaOutletProps extends React.HTMLAttributes<HTMLMediaElement> {
provider: MediaProviderInstance;
}

const YOUTUBE_TYPE = { src: '', type: 'video/youtube' },
VIMEO_TYPE = { src: '', type: 'video/vimeo' },
REMOTION_TYPE = { src: '', type: 'video/remotion' };

function MediaOutlet({ provider, ...props }: MediaOutletProps) {
const { controls, crossorigin, poster } = useStateContext(mediaState),
const { controls, crossOrigin, poster } = useStateContext(mediaState),
{ loader } = provider.$state,
{
$iosControls: $$iosControls,
Expand All @@ -80,17 +76,23 @@ function MediaOutlet({ provider, ...props }: MediaOutletProps) {
$controls = useSignal(controls),
$iosControls = useSignal($$iosControls),
$nativeControls = $controls || $iosControls,
$crossorigin = useSignal(crossorigin),
$crossOrigin = useSignal(crossOrigin),
$poster = useSignal(poster),
$loader = useSignal(loader),
$provider = useSignal($$provider),
$providerSetup = useSignal($$providerSetup),
$mediaType = $loader?.mediaType(),
isYouTubeEmbed = $loader?.canPlay(YOUTUBE_TYPE),
isVimeoEmbed = $loader?.canPlay(VIMEO_TYPE),
isEmbed = isYouTubeEmbed || isVimeoEmbed;
isYouTubeEmbed = $loader?.name === 'youtube',
isVimeoEmbed = $loader?.name === 'vimeo',
isEmbed = isYouTubeEmbed || isVimeoEmbed,
isRemotion = $loader?.name === 'remotion',
isGoogleCast = $loader?.name === 'google-cast';

if (isGoogleCast) {
return <div className="vds-google-cast" />;
}

if ($loader?.canPlay(REMOTION_TYPE)) {
if (isRemotion) {
return (
<div data-remotion-canvas>
<div
Expand Down Expand Up @@ -126,7 +128,7 @@ function MediaOutlet({ provider, ...props }: MediaOutletProps) {
? React.createElement($mediaType === 'audio' ? 'audio' : 'video', {
...props,
controls: $nativeControls ? 'true' : null,
crossOrigin: typeof $crossorigin === 'boolean' ? '' : $crossorigin,
crossOrigin: typeof $crossOrigin === 'boolean' ? '' : $crossOrigin,
poster: $mediaType === 'video' && $nativeControls && $poster ? $poster : null,
preload: 'none',
'aria-hidden': 'true',
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/components/text-track.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CaptionsFileFormat, CaptionsParserFactory } from 'media-captions';
import type { VTTJSONContent } from 'vidstack';
import type { VTTContent } from 'vidstack';

import { createTextTrack } from '../hooks/create-text-track';

Expand Down Expand Up @@ -43,7 +43,7 @@ export interface TrackProps {
/**
* Used to directly pass in text track file contents.
*/
readonly content?: string | VTTJSONContent;
readonly content?: string | VTTContent;
/**
* The captions file format to be parsed or a custom parser factory (functions that returns a
* captions parser). Supported types include: 'vtt', 'srt', 'ssa', 'ass', and 'json'.
Expand Down
48 changes: 48 additions & 0 deletions packages/react/src/components/ui/buttons/google-cast-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as React from 'react';

import { composeRefs, createReactComponent, type ReactElementProps } from 'maverick.js/react';

import { GoogleCastButtonInstance } from '../../primitives/instances';
import { Primitive } from '../../primitives/nodes';

/* -------------------------------------------------------------------------------------------------
* GoogleCastButton
* -----------------------------------------------------------------------------------------------*/

const GoogleCastButtonBridge = createReactComponent(GoogleCastButtonInstance);

export interface GoogleCastButtonProps
extends ReactElementProps<GoogleCastButtonInstance, HTMLButtonElement> {
asChild?: boolean;
children?: React.ReactNode;
ref?: React.Ref<HTMLButtonElement>;
}

/**
* A button for requesting Google Cast.
*
* @see {@link https://developers.google.com/cast/docs/overview}
* @docs {@link https://www.vidstack.io/docs/player/components/buttons/google-cast-button}
* @example
* ```tsx
* <GoogleCastButton>
* <ChromecastIcon />
* </GoogleCastButton>
* ```
*/
const GoogleCastButton = React.forwardRef<HTMLButtonElement, GoogleCastButtonProps>(
({ children, ...props }, forwardRef) => {
return (
<GoogleCastButtonBridge {...(props as Omit<GoogleCastButtonProps, 'ref'>)}>
{(props) => (
<Primitive.button {...props} ref={composeRefs(props.ref, forwardRef)}>
{children}
</Primitive.button>
)}
</GoogleCastButtonBridge>
);
},
);

GoogleCastButton.displayName = 'GoogleCastButton';
export { GoogleCastButton };
Loading

0 comments on commit c46c577

Please sign in to comment.