Skip to content

Commit

Permalink
feat(player/react): add remotion support to plyr layout
Browse files Browse the repository at this point in the history
  • Loading branch information
mihar-22 committed Jan 29, 2024
1 parent e6c506b commit 9534d68
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 46 deletions.
52 changes: 41 additions & 11 deletions packages/react/src/components/layouts/plyr/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';

import { signal } from 'maverick.js';
import { composeRefs, useSignal } from 'maverick.js/react';
import { isString, listenEvent } from 'maverick.js/std';
import { isNumber, isString, listenEvent } from 'maverick.js/std';
import type { VTTCue } from 'media-captions';
import {
isAudioSrc,
Expand All @@ -19,9 +19,11 @@ import { useAudioOptions } from '../../../hooks/options/use-audio-options';
import { useCaptionOptions } from '../../../hooks/options/use-caption-options';
import { usePlaybackRateOptions } from '../../../hooks/options/use-playback-rate-options';
import { useVideoQualityOptions } from '../../../hooks/options/use-video-quality-options';
import { useClassName } from '../../../hooks/use-dom';
import { useMediaContext } from '../../../hooks/use-media-context';
import { useMediaRemote } from '../../../hooks/use-media-remote';
import { useMediaState } from '../../../hooks/use-media-state';
import { isRemotionSource } from '../../../providers/remotion';
import { Primitive, type PrimitivePropsWithRef } from '../../primitives/nodes';
import { AirPlayButton } from '../../ui/buttons/airplay-button';
import { CaptionButton } from '../../ui/buttons/caption-button';
Expand All @@ -37,6 +39,7 @@ import * as TimeSlider from '../../ui/sliders/time-slider';
import * as VolumeSlider from '../../ui/sliders/volume-slider';
import * as Thumbnail from '../../ui/thumbnail';
import { Time } from '../../ui/time';
import { RemotionPoster, RemotionSliderThumbnail } from '../remotion-ui';
import { useLayoutName } from '../utils';
import { i18n, PlyrLayoutContext, usePlyrLayoutContext, usePlyrLayoutWord } from './context';
import { defaultPlyrLayoutProps, type PlyrLayoutProps } from './props';
Expand Down Expand Up @@ -65,20 +68,22 @@ const PlyrLayout = React.forwardRef<HTMLElement, PlyrLayoutElementProps>(
speed,
icons,
slots,
posterFrame,
className,
...elProps
} = { ...defaultPlyrLayoutProps, ...userProps },
ref = React.useRef<HTMLDivElement>(null),
[el, setEl] = React.useState<HTMLElement | null>(null),
media = useMediaContext(),
previewTime = React.useMemo(() => signal(0), []),
$viewType = useMediaState('viewType');

useLayoutName('plyr');
useClassName(el, className);

React.useEffect(() => {
const el = ref.current;
if (!el || !media) return;
usePlyrLayoutClasses(el, media);
}, [media]);
return usePlyrLayoutClasses(el, media);
}, [el, media]);

return (
<PlyrLayoutContext.Provider
Expand All @@ -98,12 +103,15 @@ const PlyrLayout = React.forwardRef<HTMLElement, PlyrLayoutElementProps>(
previewTime,
icons,
slots,
posterFrame,
}}
>
<Primitive.div
{...elProps}
className={`plyr plyr--full-ui plyr--${$viewType} ${elProps.className}`}
ref={composeRefs(ref, forwardRef) as any}
className={
__SERVER__ ? `plyr plyr--full-ui plyr--${$viewType} ${className || ''}` : undefined
}
ref={composeRefs(setEl, forwardRef) as any}
>
{$viewType === 'audio' ? (
<PlyrAudioLayout />
Expand Down Expand Up @@ -209,10 +217,19 @@ PlyrPreviewScrubbing.displayName = 'PlyrPreviewScrubbing';
* -----------------------------------------------------------------------------------------------*/

function PlyrPoster() {
const $poster = useMediaState('poster');
const $src = useMediaState('source'),
$poster = useMediaState('poster'),
{ posterFrame } = usePlyrLayoutContext(),
$RemotionPoster = useSignal(RemotionPoster),
$hasRemotionPoster = $RemotionPoster && isRemotionSource($src) && isNumber(posterFrame);

return slot(
'poster',
<div className="plyr__poster" style={{ backgroundImage: `url("${$poster}")` }} />,
$hasRemotionPoster ? (
<$RemotionPoster frame={posterFrame} className="plyr__poster" />
) : (
<div className="plyr__poster" style={{ backgroundImage: `url("${$poster}")` }} />
),
);
}

Expand Down Expand Up @@ -516,9 +533,12 @@ PlyrRewindButton.displayName = 'PlyrRewindButton';

function PlyrTimeSlider() {
const { markers, thumbnails, seekTime, previewTime } = usePlyrLayoutContext(),
$src = useMediaState('source'),
$duration = useMediaState('duration'),
seekText = usePlyrLayoutWord('Seek'),
[activeMarker, setActiveMarker] = React.useState<PlyrMarker | null>(null);
[activeMarker, setActiveMarker] = React.useState<PlyrMarker | null>(null),
$RemotionSliderThumbnail = useSignal(RemotionSliderThumbnail),
$hasRemotionSliderThumbnail = $RemotionSliderThumbnail && isRemotionSource($src);

function onSeekingRequest(time: number) {
previewTime.set(time);
Expand Down Expand Up @@ -555,11 +575,21 @@ function PlyrTimeSlider() {
<div className="plyr__slider__thumb"></div>
<div className="plyr__slider__buffer"></div>

{!thumbnails ? (
{!thumbnails && !$hasRemotionSliderThumbnail ? (
<span className="plyr__tooltip">
{markerLabel}
<TimeSlider.Value />
</span>
) : $hasRemotionSliderThumbnail ? (
<TimeSlider.Preview className="plyr__slider__preview">
<div className="plyr__slider__preview__thumbnail">
<span className="plyr__slider__preview__time-container">
{markerLabel}
<TimeSlider.Value className="plyr__slider__preview__time" />
</span>
<$RemotionSliderThumbnail className="plyr__slider__preview__thumbnail" />
</div>
</TimeSlider.Preview>
) : (
<TimeSlider.Preview className="plyr__slider__preview">
<TimeSlider.Thumbnail.Root
Expand Down
7 changes: 6 additions & 1 deletion packages/react/src/components/layouts/plyr/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { PlyrLayoutProps as BaseProps } from 'vidstack';
import type { PlyrLayoutSlots } from '.';
import type { PlyrLayoutIcons } from './icons';

export const defaultPlyrLayoutProps: Omit<PlyrLayoutProps, 'icons' | 'slots'> = {
export const defaultPlyrLayoutProps: Omit<PlyrLayoutProps, 'icons' | 'slots' | 'posterFrame'> = {
clickToPlay: true,
clickToFullscreen: true,
controls: [
Expand Down Expand Up @@ -38,4 +38,9 @@ export interface PlyrLayoutProps extends Omit<Partial<BaseProps>, 'customIcons'>
* Provide additional content to be inserted in specific positions.
*/
slots?: PlyrLayoutSlots;
/**
* The frame of the video to use as the poster. This only works with Remotion sources at the
* moment.
*/
posterFrame?: number;
}
4 changes: 4 additions & 0 deletions packages/react/src/components/layouts/remotion-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ export const RemotionThumbnail = signal<React.LazyExoticComponent<React.Componen
export const RemotionSliderThumbnail = signal<React.LazyExoticComponent<
React.ComponentType<any>
> | null>(null);

export const RemotionPoster = signal<React.LazyExoticComponent<React.ComponentType<any>> | null>(
null,
);
13 changes: 13 additions & 0 deletions packages/react/src/hooks/use-dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@ import * as React from 'react';

import { animationFrameThrottle, createDisposalBin, listenEvent, setStyle } from 'maverick.js/std';

export function useClassName(el: HTMLElement | null, className?: string) {
React.useEffect(() => {
if (!el || !className) return;

const tokens = className.split(' ');
for (const token of tokens) el.classList.add(token);

return () => {
for (const token of tokens) el.classList.remove(token);
};
}, [el, className]);
}

export function useResizeObserver(el: Element | null | undefined, callback: () => void) {
React.useEffect(() => {
if (!el) return;
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/providers/remotion/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class RemotionProviderLoader implements MediaProviderLoader {
constructor() {
UI.RemotionThumbnail.set(React.lazy(() => import('./ui/thumbnail')));
UI.RemotionSliderThumbnail.set(React.lazy(() => import('./ui/slider-thumbnail')));
UI.RemotionPoster.set(React.lazy(() => import('./ui/poster')));
}

canPlay(src: MediaSrc): boolean {
Expand Down
3 changes: 2 additions & 1 deletion packages/vidstack/player/styles/plyr/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,8 @@ a.plyr__control::before {
pointer-events: none;
}

.plyr--stopped.plyr__poster-enabled .plyr__poster {
.plyr--stopped.plyr__poster-enabled .plyr__poster,
.plyr__poster[data-remotion-poster][data-visible] {
opacity: 1;
}

Expand Down
71 changes: 38 additions & 33 deletions packages/vidstack/src/components/layouts/plyr/plyr-layout.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Component, effect, provideContext, signal } from 'maverick.js';
import { createDisposalBin } from 'maverick.js/std';

import { useMediaContext, type MediaContext } from '../../../core/api/media-context';
import { plyrLayoutContext } from './context';
Expand All @@ -20,25 +21,24 @@ export class PlyrLayout extends Component<PlyrLayoutProps> {
}

export function usePlyrLayoutClasses(el: HTMLElement, media: MediaContext) {
const { $provider } = media,
{
fullscreen,
canFullscreen,
canPictureInPicture,
pictureInPicture,
hasCaptions,
textTrack,
canAirPlay,
isAirPlayConnected,
viewType,
playing,
paused,
controlsVisible,
pointer,
waiting,
currentTime,
poster,
} = media.$state;
const {
fullscreen,
canFullscreen,
canPictureInPicture,
pictureInPicture,
hasCaptions,
textTrack,
canAirPlay,
isAirPlayConnected,
viewType,
playing,
paused,
controlsVisible,
pointer,
waiting,
currentTime,
poster,
} = media.$state;

el.classList.add('plyr');
el.classList.add('plyr--full-ui');
Expand All @@ -61,23 +61,28 @@ export function usePlyrLayoutClasses(el: HTMLElement, media: MediaContext) {
'plyr--captions-enabled': hasCaptions,
};

for (const key of Object.keys(classes)) {
effect(() => void el.classList.toggle(key, !!classes[key]()));
const disposal = createDisposalBin();

for (const token of Object.keys(classes)) {
disposal.add(effect(() => void el.classList.toggle(token, !!classes[token]())));
}

effect(() => {
const token = `plyr--${viewType()}`;
el.classList.add(token);
return () => el.classList.remove(token);
});
disposal.add(
effect(() => {
const token = `plyr--${viewType()}`;
el.classList.add(token);
return () => el.classList.remove(token);
}),
effect(() => {
const { $provider } = media,
type = $provider()?.type,
token = `plyr--${isHTMLProvider(type) ? 'html5' : type}`;
el.classList.toggle(token, !!type);
return () => el.classList.remove(token);
}),
);

effect(() => {
const { $provider } = media,
type = $provider()?.type,
token = `plyr--${isHTMLProvider(type) ? 'html5' : type}`;
el.classList.toggle(token, !!type);
return () => el.classList.remove(token);
});
return () => disposal.empty();
}

function isHTMLProvider(type: string | undefined) {
Expand Down

0 comments on commit 9534d68

Please sign in to comment.