Skip to content

Commit

Permalink
Merge pull request #1382 from DerrykBoyd/issue/1300/loop-prop-video
Browse files Browse the repository at this point in the history
  • Loading branch information
JonnyBurger committed Oct 11, 2022
2 parents 6dd2be7 + 25cf192 commit b579cc2
Show file tree
Hide file tree
Showing 20 changed files with 425 additions and 90 deletions.
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

1 comment on commit b579cc2

@vercel
Copy link

@vercel vercel bot commented on b579cc2 Oct 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.