Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add loop prop for Video and Audio components #1382

Merged
merged 18 commits into from
Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/core/src/video/OffthreadVideo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {useCallback} from 'react';
import {getRemotionEnvironment} from '../get-environment';
import {Sequence} from '../Sequence';
import {validateMediaProps} from '../validate-media-props';
Expand All @@ -13,6 +13,8 @@ export const OffthreadVideo: React.FC<
> = (props) => {
const {startFrom, endAt, ...otherProps} = props;

const onDuration = useCallback(() => undefined, []);

if (typeof startFrom !== 'undefined' || typeof endAt !== 'undefined') {
validateStartFromProps(startFrom, endAt);

Expand All @@ -37,5 +39,11 @@ export const OffthreadVideo: React.FC<
return <OffthreadVideoForRendering {...otherProps} />;
}

return <VideoForDevelopment onlyWarnForMediaSeekingError {...otherProps} />;
return (
<VideoForDevelopment
onDuration={onDuration}
onlyWarnForMediaSeekingError
{...otherProps}
/>
);
};
26 changes: 24 additions & 2 deletions packages/core/src/video/Video.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React, {forwardRef} from 'react';
import React, {forwardRef, useCallback, useReducer} from 'react';
import {getRemotionEnvironment} from '../get-environment';
import {Loop} from '../loop';
import {Sequence} from '../Sequence';
import {useVideoConfig} from '../use-video-config';
import {validateMediaProps} from '../validate-media-props';
import {validateStartFromProps} from '../validate-start-from-props';
import {durationReducer} from './duration-state';
import type {RemotionMainVideoProps, RemotionVideoProps} from './props';
import {VideoForDevelopment} from './VideoForDevelopment';
import {VideoForRendering} from './VideoForRendering';
Expand All @@ -12,11 +15,27 @@ const VideoForwardingFunction: React.ForwardRefRenderFunction<
RemotionVideoProps & RemotionMainVideoProps
> = (props, ref) => {
const {startFrom, endAt, ...otherProps} = props;
const {loop, ...propsOtherThanLoop} = props;
const {fps} = useVideoConfig();

const [durations, setDurations] = useReducer(durationReducer, {});

if (typeof ref === 'string') {
throw new Error('string refs are not supported');
}

const onDuration = useCallback((src: string, durationInSeconds: number) => {
setDurations({type: 'got-duration', durationInSeconds, src});
}, []);

if (loop && props.src && durations[props.src as string] !== undefined) {
return (
<Loop durationInFrames={Math.round(durations[props.src as string] * fps)}>
<Video {...propsOtherThanLoop} ref={ref} />
</Loop>
);
}

if (typeof startFrom !== 'undefined' || typeof endAt !== 'undefined') {
validateStartFromProps(startFrom, endAt);

Expand All @@ -37,14 +56,17 @@ const VideoForwardingFunction: React.ForwardRefRenderFunction<
validateMediaProps(props, 'Video');

if (getRemotionEnvironment() === 'rendering') {
return <VideoForRendering {...otherProps} ref={ref} />;
return (
<VideoForRendering onDuration={onDuration} {...otherProps} ref={ref} />
);
}

return (
<VideoForDevelopment
onlyWarnForMediaSeekingError={false}
{...otherProps}
ref={ref}
onDuration={onDuration}
/>
);
};
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/video/VideoForDevelopment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {RemotionVideoProps} from './props';

type VideoForDevelopmentProps = RemotionVideoProps & {
onlyWarnForMediaSeekingError: boolean;
onDuration: (src: string, durationInSeconds: number) => void;
};

const VideoForDevelopmentRefForwardingFunction: React.ForwardRefRenderFunction<
Expand All @@ -30,6 +31,7 @@ const VideoForDevelopmentRefForwardingFunction: React.ForwardRefRenderFunction<
playbackRate,
onlyWarnForMediaSeekingError,
src,
onDuration,
...nativeProps
} = props;

Expand Down Expand Up @@ -78,6 +80,10 @@ const VideoForDevelopmentRefForwardingFunction: React.ForwardRefRenderFunction<
return;
}

current.onloadedmetadata = () => {
onDuration(src as string, current.duration);
};

const errorHandler = () => {
if (current?.error) {
console.error('Error occurred in video', current?.error);
Expand All @@ -93,7 +99,7 @@ const VideoForDevelopmentRefForwardingFunction: React.ForwardRefRenderFunction<
return () => {
current.removeEventListener('error', errorHandler);
};
}, [src]);
}, [src, onDuration]);

return (
<video
Expand Down
46 changes: 44 additions & 2 deletions packages/core/src/video/VideoForRendering.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, {
useContext,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
} from 'react';
Expand All @@ -14,6 +15,7 @@ import {
} from '../audio/use-audio-frame';
import {CompositionManager} from '../CompositionManager';
import {continueRender, delayRender} from '../delay-render';
import {getRemotionEnvironment} from '../get-environment';
import {isApproximatelyTheSame} from '../is-approximately-the-same';
import {random} from '../random';
import {SequenceContext} from '../Sequence';
Expand All @@ -25,9 +27,13 @@ import {warnAboutNonSeekableMedia} from '../warn-about-non-seekable-media';
import {getMediaTime} from './get-current-time';
import type {RemotionVideoProps} from './props';

type VideoForRenderingProps = RemotionVideoProps & {
onDuration: (src: string, durationInSeconds: number) => void;
};

const VideoForRenderingForwardFunction: React.ForwardRefRenderFunction<
HTMLVideoElement,
RemotionVideoProps
VideoForRenderingProps
> = ({onError, volume: volumeProp, playbackRate, ...props}, ref) => {
const absoluteFrame = useTimelinePosition();

Expand Down Expand Up @@ -210,11 +216,47 @@ const VideoForRenderingForwardFunction: React.ForwardRefRenderFunction<
mediaStartsAt,
]);

const {src, onDuration} = props;

// If video source switches, make new handle
if (getRemotionEnvironment() === 'rendering') {
// eslint-disable-next-line react-hooks/rules-of-hooks
useLayoutEffect(() => {
if (process.env.NODE_ENV === 'test') {
return;
}

const newHandle = delayRender('Loading <Video> duration with src=' + src);
const {current} = videoRef;

const didLoad = () => {
if (current) {
onDuration(src as string, current.duration);
}

continueRender(newHandle);
};

if (current?.duration) {
onDuration(src as string, current.duration);
continueRender(newHandle);
} else {
current?.addEventListener('loadedmetadata', didLoad, {once: true});
}

// If tag gets unmounted, clear pending handles because video metadata is not going to load
return () => {
current?.removeEventListener('loadedmetadata', didLoad);
continueRender(newHandle);
};
}, [src, onDuration]);
}

return <video ref={videoRef} {...props} onError={onError} />;
};

export const VideoForRendering = forwardRef(
VideoForRenderingForwardFunction
) as ForwardRefExoticComponent<
RemotionVideoProps & RefAttributes<HTMLVideoElement>
VideoForRenderingProps & RefAttributes<HTMLVideoElement>
>;
22 changes: 22 additions & 0 deletions packages/core/src/video/duration-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
type DurationState = Record<string, number>;

type DurationAction = {
type: 'got-duration';
src: string;
durationInSeconds: number;
};

export const durationReducer = (
state: DurationState,
action: DurationAction
) => {
switch (action.type) {
case 'got-duration':
return {
...state,
[action.src]: action.durationInSeconds,
};
default:
return state;
}
};
2 changes: 1 addition & 1 deletion packages/core/src/video/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type RemotionVideoProps = Omit<
React.VideoHTMLAttributes<HTMLVideoElement>,
HTMLVideoElement
>,
'autoPlay' | 'controls' | 'loop' | 'onEnded'
'autoPlay' | 'controls' | 'onEnded'
> & {
volume?: VolumeProp;
playbackRate?: number;
Expand Down
3 changes: 2 additions & 1 deletion packages/example/src/Video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ export const Index: React.FC = () => {
width={1080}
height={1080}
fps={30}
durationInFrames={100}
// Longer value to test looping
durationInFrames={250}
defaultProps={{
offthread: false,
codec: 'mp4' as const,
Expand Down
2 changes: 1 addition & 1 deletion packages/example/src/VideoTesting/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const VideoTesting: React.FC<{
return (
<div>
<Sequence from={0} durationInFrames={durationInFrames}>
<Comp src={codec === 'mp4' ? videoMp4 : videoWebm} />
<Comp loop src={codec === 'mp4' ? videoMp4 : videoWebm} />
</Sequence>
</div>
);
Expand Down