UNPKG

@remotion/player

Version:

React component for embedding a Remotion preview into your app

158 lines (157 loc) • 5.83 kB
/* 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]); };