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 all 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
15 changes: 9 additions & 6 deletions packages/core/src/RemotionRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
TimelineContextValue,
} from './timeline-position-state';
import {SetTimelineContext, TimelineContext} from './timeline-position-state';
import {DurationsContextProvider} from './video/duration-state';

export const RemotionRoot: React.FC<{
children: React.ReactNode;
Expand Down Expand Up @@ -85,12 +86,14 @@ export const RemotionRoot: React.FC<{
<SetTimelineContext.Provider value={setTimelineContextValue}>
<PrefetchProvider>
<CompositionManagerProvider>
<SharedAudioContextProvider
// In the preview, which is mostly played on Desktop, we opt out of the autoplay policy fix as described in https://github.com/remotion-dev/remotion/pull/554, as it mostly applies to mobile.
numberOfAudioTags={0}
>
{children}
</SharedAudioContextProvider>
<DurationsContextProvider>
<SharedAudioContextProvider
// In the preview, which is mostly played on Desktop, we opt out of the autoplay policy fix as described in https://github.com/remotion-dev/remotion/pull/554, as it mostly applies to mobile.
numberOfAudioTags={0}
>
{children}
</SharedAudioContextProvider>
</DurationsContextProvider>
</CompositionManagerProvider>
</PrefetchProvider>
</SetTimelineContext.Provider>
Expand Down
34 changes: 33 additions & 1 deletion packages/core/src/audio/Audio.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React, {forwardRef, useCallback, useContext} 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 {DurationsContext} from '../video/duration-state';
import {AudioForDevelopment} from './AudioForDevelopment';
import {AudioForRendering} from './AudioForRendering';
import type {RemotionAudioProps, RemotionMainAudioProps} from './props';
Expand All @@ -14,6 +17,10 @@ const AudioRefForwardingFunction: React.ForwardRefRenderFunction<
> = (props, ref) => {
const audioContext = useContext(SharedAudioContext);
const {startFrom, endAt, ...otherProps} = props;
const {loop, ...propsOtherThanLoop} = props;
const {fps} = useVideoConfig();

const {durations, setDurations} = useContext(DurationsContext);

const onError: React.ReactEventHandler<HTMLAudioElement> = useCallback(
(e) => {
Expand All @@ -25,6 +32,23 @@ const AudioRefForwardingFunction: React.ForwardRefRenderFunction<
[otherProps.src]
);

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

if (loop && props.src && durations[props.src as string] !== undefined) {
const duration = Math.floor(durations[props.src as string] * fps);

return (
<Loop layout="none" durationInFrames={duration}>
<Audio {...propsOtherThanLoop} ref={ref} />
</Loop>
);
}

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

Expand All @@ -45,7 +69,14 @@ const AudioRefForwardingFunction: React.ForwardRefRenderFunction<
validateMediaProps(props, 'Audio');

if (getRemotionEnvironment() === 'rendering') {
return <AudioForRendering {...props} ref={ref} onError={onError} />;
return (
<AudioForRendering
onDuration={onDuration}
{...props}
ref={ref}
onError={onError}
/>
);
}

return (
Expand All @@ -56,6 +87,7 @@ const AudioRefForwardingFunction: React.ForwardRefRenderFunction<
{...props}
ref={ref}
onError={onError}
onDuration={onDuration}
/>
);
};
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/audio/AudioForDevelopment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import type {ForwardRefExoticComponent, RefAttributes} from 'react';
import React, {
forwardRef,
useContext,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import {usePreload} from '../prefetch';
Expand All @@ -23,6 +25,7 @@ import {useFrameForVolumeProp} from './use-audio-frame';

type AudioForDevelopmentProps = RemotionAudioProps & {
shouldPreMountAudioTags: boolean;
onDuration: (src: string, durationInSeconds: number) => void;
};

const AudioForDevelopmentForwardRefFunction: React.ForwardRefRenderFunction<
Expand All @@ -49,6 +52,7 @@ const AudioForDevelopmentForwardRefFunction: React.ForwardRefRenderFunction<
playbackRate,
shouldPreMountAudioTags,
src,
onDuration,
...nativeProps
} = props;

Expand Down Expand Up @@ -114,6 +118,31 @@ const AudioForDevelopmentForwardRefFunction: React.ForwardRefRenderFunction<
[audioRef]
);

const currentOnDurationCallback =
useRef<AudioForDevelopmentProps['onDuration']>();
currentOnDurationCallback.current = onDuration;

useEffect(() => {
const {current} = audioRef;
if (!current) {
return;
}

if (current.duration) {
currentOnDurationCallback.current?.(src, current.duration);
return;
}

const onLoadedMetadata = () => {
currentOnDurationCallback.current?.(src, current.duration);
};

current.addEventListener('loadedmetadata', onLoadedMetadata);
return () => {
current.removeEventListener('loadedmetadata', onLoadedMetadata);
};
}, [audioRef, src]);

if (initialShouldPreMountAudioElements) {
return null;
}
Expand Down
47 changes: 45 additions & 2 deletions packages/core/src/audio/AudioForRendering.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import React, {
useContext,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
} from 'react';
import {getAbsoluteSrc} from '../absolute-src';
import {CompositionManager} from '../CompositionManager';
import {continueRender, delayRender} from '../delay-render';
import {getRemotionEnvironment} from '../get-environment';
import {random} from '../random';
import {SequenceContext} from '../Sequence';
import {useTimelinePosition} from '../timeline-position-state';
Expand All @@ -17,9 +20,13 @@ import {evaluateVolume} from '../volume-prop';
import type {RemotionAudioProps} from './props';
import {useFrameForVolumeProp} from './use-audio-frame';

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

const AudioForRenderingRefForwardingFunction: React.ForwardRefRenderFunction<
HTMLAudioElement,
RemotionAudioProps
AudioForRenderingProps
> = (props, ref) => {
const audioRef = useRef<HTMLAudioElement>(null);

Expand Down Expand Up @@ -96,11 +103,47 @@ const AudioForRenderingRefForwardingFunction: React.ForwardRefRenderFunction<
props.playbackRate,
]);

const {src, onDuration} = props;

// If audio 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 <Audio> duration with src=' + src);
const {current} = audioRef;

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 <audio ref={audioRef} {...nativeProps} />;
};

export const AudioForRendering = forwardRef(
AudioForRenderingRefForwardingFunction
) as ForwardRefExoticComponent<
RemotionAudioProps & RefAttributes<HTMLAudioElement>
AudioForRenderingProps & RefAttributes<HTMLAudioElement>
>;
2 changes: 1 addition & 1 deletion packages/core/src/audio/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type RemotionAudioProps = Omit<
React.AudioHTMLAttributes<HTMLAudioElement>,
HTMLAudioElement
>,
'autoPlay' | 'controls' | 'loop' | 'onEnded'
'autoPlay' | 'controls' | 'onEnded'
> & {
volume?: VolumeProp;
playbackRate?: number;
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {validateDimension} from './validation/validate-dimensions';
import {validateDurationInFrames} from './validation/validate-duration-in-frames';
import {validateFps} from './validation/validate-fps';
import {validateOffthreadVideoImageFormat} from './validation/validate-offthreadvideo-image-format';
import {DurationsContextProvider} from './video/duration-state';
import type {
MediaVolumeContextValue,
SetMediaVolumeContextValue,
Expand Down Expand Up @@ -101,6 +102,7 @@ export const Internals = {
CanUseRemotionHooks,
enableLegacyRemotionConfig,
PrefetchProvider,
DurationsContextProvider,
};

type WebpackConfiguration = Configuration;
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/play-and-handle-not-allowed-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export const playAndHandleNotAllowedError = (
return;
}

// Audio tag got unmounted
if (
err.message.includes('because the media was removed from the document')
) {
return;
}

console.log(`Could not play ${mediaType} due to following error: `, err);
if (!current.muted) {
console.log(`The video will be muted and we'll retry playing it.`, err);
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/test/audio-for-rendering.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ describe('Register and unregister asset', () => {
src: 'test',
muted: false,
volume: 50,
onDuration: vitest.fn(),
};
const {unmount} = render(
<CanUseRemotionHooksProvider>
Expand All @@ -85,6 +86,7 @@ describe('Register and unregister asset', () => {
src: undefined,
muted: false,
volume: 50,
onDuration: vitest.fn(),
};
expectToThrow(() => {
render(
Expand Down Expand Up @@ -117,6 +119,7 @@ describe('useEffect tests', () => {
src: 'test',
muted: false,
volume: 50,
onDuration: vitest.fn(),
};
render(
<CanUseRemotionHooksProvider>
Expand Down
14 changes: 11 additions & 3 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 @@ -9,10 +9,12 @@ import type {OffthreadVideoProps, RemotionMainVideoProps} from './props';
import {VideoForDevelopment} from './VideoForDevelopment';

export const OffthreadVideo: React.FC<
OffthreadVideoProps & RemotionMainVideoProps
Omit<OffthreadVideoProps & RemotionMainVideoProps, 'loop'>
> = (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}
/>
);
};
29 changes: 27 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, useContext} 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 {DurationsContext} from './duration-state';
import type {RemotionMainVideoProps, RemotionVideoProps} from './props';
import {VideoForDevelopment} from './VideoForDevelopment';
import {VideoForRendering} from './VideoForRendering';
Expand All @@ -12,11 +15,30 @@ const VideoForwardingFunction: React.ForwardRefRenderFunction<
RemotionVideoProps & RemotionMainVideoProps
> = (props, ref) => {
const {startFrom, endAt, ...otherProps} = props;
const {loop, ...propsOtherThanLoop} = props;
const {fps} = useVideoConfig();

const {durations, setDurations} = useContext(DurationsContext);

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});
},
[setDurations]
);

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 +59,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