@remotion/player
Version:
React component for embedding a Remotion preview into your app
158 lines (157 loc) • 5.83 kB
JavaScript
/* eslint-disable @typescript-eslint/no-use-before-define */
import { useContext, useEffect, useRef } from 'react';
import { Internals } from 'remotion';
import { calculateNextFrame } from './calculate-next-frame.js';
import { useIsBackgrounded } from './is-backgrounded.js';
import { usePlayer } from './use-player.js';
export const usePlayback = ({ loop, playbackRate, moveToBeginningWhenEnded, inFrame, outFrame, frameRef, }) => {
const config = Internals.useUnsafeVideoConfig();
const frame = Internals.Timeline.useTimelinePosition();
const { playing, pause, emitter } = usePlayer();
const setFrame = Internals.Timeline.useTimelineSetFrame();
const buffering = useRef(null);
// requestAnimationFrame() does not work if the tab is not active.
// This means that audio will keep playing even if it has ended.
// In that case, we use setTimeout() instead.
const isBackgroundedRef = useIsBackgrounded();
const lastTimeUpdateEvent = useRef(null);
const context = useContext(Internals.BufferingContextReact);
if (!context) {
throw new Error('Missing the buffering context. Most likely you have a Remotion version mismatch.');
}
useEffect(() => {
const onBufferClear = context.listenForBuffering(() => {
buffering.current = performance.now();
});
const onResumeClear = context.listenForResume(() => {
buffering.current = null;
});
return () => {
onBufferClear.remove();
onResumeClear.remove();
};
}, [context]);
useEffect(() => {
if (!config) {
return;
}
if (!playing) {
return;
}
let hasBeenStopped = false;
let reqAnimFrameCall = null;
let startedTime = performance.now();
let framesAdvanced = 0;
const cancelQueuedFrame = () => {
if (reqAnimFrameCall !== null) {
if (reqAnimFrameCall.type === 'raf') {
cancelAnimationFrame(reqAnimFrameCall.id);
}
else {
clearTimeout(reqAnimFrameCall.id);
}
}
};
const stop = () => {
hasBeenStopped = true;
cancelQueuedFrame();
};
const callback = () => {
const time = performance.now() - startedTime;
const actualLastFrame = outFrame !== null && outFrame !== void 0 ? outFrame : config.durationInFrames - 1;
const actualFirstFrame = inFrame !== null && inFrame !== void 0 ? inFrame : 0;
const currentFrame = frameRef.current;
const { nextFrame, framesToAdvance, hasEnded } = calculateNextFrame({
time,
currentFrame,
playbackSpeed: playbackRate,
fps: config.fps,
actualFirstFrame,
actualLastFrame,
framesAdvanced,
shouldLoop: loop,
});
framesAdvanced += framesToAdvance;
if (nextFrame !== frameRef.current &&
(!hasEnded || moveToBeginningWhenEnded)) {
setFrame((c) => ({ ...c, [config.id]: nextFrame }));
}
if (hasEnded) {
stop();
pause();
emitter.dispatchEnded();
return;
}
if (!hasBeenStopped) {
queueNextFrame();
}
};
const queueNextFrame = () => {
if (buffering.current) {
const stopListening = context.listenForResume(() => {
stopListening.remove();
if (hasBeenStopped) {
return;
}
startedTime = performance.now();
framesAdvanced = 0;
callback();
});
return;
}
if (isBackgroundedRef.current) {
reqAnimFrameCall = {
type: 'timeout',
// Note: Most likely, this will not be 1000 / fps, but the browser will throttle it to ~1/sec.
id: setTimeout(callback, 1000 / config.fps),
};
}
else {
reqAnimFrameCall = { type: 'raf', id: requestAnimationFrame(callback) };
}
};
queueNextFrame();
const onVisibilityChange = () => {
if (document.visibilityState === 'visible') {
return;
}
// If tab goes into the background, cancel requestAnimationFrame() and update immediately.
// , so the transition to setTimeout() can be fulfilled.
cancelQueuedFrame();
callback();
};
window.addEventListener('visibilitychange', onVisibilityChange);
return () => {
window.removeEventListener('visibilitychange', onVisibilityChange);
stop();
};
}, [
config,
loop,
pause,
playing,
setFrame,
emitter,
playbackRate,
inFrame,
outFrame,
moveToBeginningWhenEnded,
isBackgroundedRef,
frameRef,
buffering,
context,
]);
useEffect(() => {
const interval = setInterval(() => {
if (lastTimeUpdateEvent.current === frameRef.current) {
return;
}
emitter.dispatchTimeUpdate({ frame: frameRef.current });
lastTimeUpdateEvent.current = frameRef.current;
}, 250);
return () => clearInterval(interval);
}, [emitter, frameRef]);
useEffect(() => {
emitter.dispatchFrameUpdate({ frame });
}, [emitter, frame]);
};