Display a buffer state
Since Remotion v4.0.111, Remotion has a native buffer state. The technique described on this page should only be used for older versions of Remotion.
In your <Player>
, you might have videos and other assets that might take some time to load after they enter the scene.
You can preload those assets, but sometimes browser policies prevent preloading and a brief flash is possible while the browser needs to decode the video before playing.
In this case, you might want to pause the Player if media is loading and show a spinner, and unpause the video once the media is ready to play. This can be implemented using regular Web APIs and React primitives.
Reference application
Visit this GitHub repo to see a fully functioning example of this technique.
Implementing a buffer state
We create a new React Context that can handle the buffering states of media inside our Player. We implement default functions that do nothing, since no buffer state is necessary during rendering.
BufferManager.tsxtsx
import { createContext } from "react";type BufferState = { [key: string]: boolean };type BufferContextType = {canPlay: (id: string) => void;needsToBuffer: (id: string) => void;};export const BufferContext = createContext<BufferContextType>({// By default, do nothing if the context is not set, for example in renderingcanPlay: () => {},needsToBuffer: () => {},});
BufferManager.tsxtsx
import { createContext } from "react";type BufferState = { [key: string]: boolean };type BufferContextType = {canPlay: (id: string) => void;needsToBuffer: (id: string) => void;};export const BufferContext = createContext<BufferContextType>({// By default, do nothing if the context is not set, for example in renderingcanPlay: () => {},needsToBuffer: () => {},});
The following component can be wrapped around the Player to provide it with the onBuffer
and onContinue
functions. By using a context, we don't have to pass those functions as props to every media element, even though it is also possible.
If one media element is buffering, it can register that to the manager using onBuffer()
. If all media elements are loaded, the buffer manager will call the onContinue()
event.
BufferManager.tsxtsx
import {useCallback ,useMemo ,useRef } from "react";export constBufferManager :React .FC <{children :React .ReactNode ;onBuffer : () => void;onContinue : () => void;}> = ({children ,onBuffer ,onContinue }) => {constbufferState =useRef <BufferState >({});constcurrentState =useRef (false);constsendEvents =useCallback (() => {letpreviousState =currentState .current ;currentState .current =Object .values (bufferState .current ).some (Boolean );if (currentState .current && !previousState ) {onBuffer ();} else if (!currentState .current &&previousState ) {onContinue ();}}, [onBuffer ,onContinue ]);constcanPlay =useCallback ((id : string) => {bufferState .current [id ] = false;sendEvents ();},[sendEvents ],);constneedsToBuffer =useCallback ((id : string) => {bufferState .current [id ] = true;sendEvents ();},[sendEvents ],);constbufferEvents =useMemo (() => {return {canPlay ,needsToBuffer ,};}, [canPlay ,needsToBuffer ]);return (<BufferContext .Provider value ={bufferEvents }>{children }</BufferContext .Provider >);};
BufferManager.tsxtsx
import {useCallback ,useMemo ,useRef } from "react";export constBufferManager :React .FC <{children :React .ReactNode ;onBuffer : () => void;onContinue : () => void;}> = ({children ,onBuffer ,onContinue }) => {constbufferState =useRef <BufferState >({});constcurrentState =useRef (false);constsendEvents =useCallback (() => {letpreviousState =currentState .current ;currentState .current =Object .values (bufferState .current ).some (Boolean );if (currentState .current && !previousState ) {onBuffer ();} else if (!currentState .current &&previousState ) {onContinue ();}}, [onBuffer ,onContinue ]);constcanPlay =useCallback ((id : string) => {bufferState .current [id ] = false;sendEvents ();},[sendEvents ],);constneedsToBuffer =useCallback ((id : string) => {bufferState .current [id ] = true;sendEvents ();},[sendEvents ],);constbufferEvents =useMemo (() => {return {canPlay ,needsToBuffer ,};}, [canPlay ,needsToBuffer ]);return (<BufferContext .Provider value ={bufferEvents }>{children }</BufferContext .Provider >);};
Making the <Video>
report buffering
The following component <PausableVideo>
wraps the <Video>
tag, so that you can use it instead of it. It grabs the context we have defined beforehand and reports buffering and resuming of the video to the BufferManager
.
PausableVideo.tsxtsx
importReact , {forwardRef ,useContext ,useEffect ,useId ,useImperativeHandle ,useRef ,} from "react";import {RemotionMainVideoProps ,RemotionVideoProps ,Video } from "remotion";import {BufferContext } from "./BufferManager";constPausableVideoFunction :React .ForwardRefRenderFunction <HTMLVideoElement ,RemotionVideoProps &RemotionMainVideoProps > = ({src , ...props },ref ) => {constvideoRef =useRef <HTMLVideoElement >(null);constid =useId ();useImperativeHandle (ref , () =>videoRef .current asHTMLVideoElement );const {canPlay ,needsToBuffer } =useContext (BufferContext );useEffect (() => {const {current } =videoRef ;if (!current ) {return;}constonPlay = () => {canPlay (id );};constonBuffer = () => {needsToBuffer (id );};current .addEventListener ("canplay",onPlay );current .addEventListener ("waiting",onBuffer );return () => {current .removeEventListener ("canplay",onPlay );current .removeEventListener ("waiting",onBuffer );// If component is unmounted, unblock the buffer managercanPlay (id );};}, [canPlay ,id ,needsToBuffer ]);return <Video {...props }ref ={videoRef }src ={src } />;};export constPausableVideo =forwardRef (PausableVideoFunction );
PausableVideo.tsxtsx
importReact , {forwardRef ,useContext ,useEffect ,useId ,useImperativeHandle ,useRef ,} from "react";import {RemotionMainVideoProps ,RemotionVideoProps ,Video } from "remotion";import {BufferContext } from "./BufferManager";constPausableVideoFunction :React .ForwardRefRenderFunction <HTMLVideoElement ,RemotionVideoProps &RemotionMainVideoProps > = ({src , ...props },ref ) => {constvideoRef =useRef <HTMLVideoElement >(null);constid =useId ();useImperativeHandle (ref , () =>videoRef .current asHTMLVideoElement );const {canPlay ,needsToBuffer } =useContext (BufferContext );useEffect (() => {const {current } =videoRef ;if (!current ) {return;}constonPlay = () => {canPlay (id );};constonBuffer = () => {needsToBuffer (id );};current .addEventListener ("canplay",onPlay );current .addEventListener ("waiting",onBuffer );return () => {current .removeEventListener ("canplay",onPlay );current .removeEventListener ("waiting",onBuffer );// If component is unmounted, unblock the buffer managercanPlay (id );};}, [canPlay ,id ,needsToBuffer ]);return <Video {...props }ref ={videoRef }src ={src } />;};export constPausableVideo =forwardRef (PausableVideoFunction );
If you are using <OffthreadVideo>
instead, you cannot have a ref attached to it.
Use this technique to use <OffthreadVideo>
only during rendering.
Replace <Video>
elements in your Remotion component with <PausableVideoFunction>
to make them report buffering state.
Pause video and display loading UI
Wrap your Player in the newly created <BufferManager>
. Create two functions onBuffer
and onContinue
that implement what should happen if the video goes into a buffering state. Pass them to the <BufferManager>
.
In this example, a ref is being used to track whether the video was paused due to buffering, so that the video will only be resumed in that case.
By using a ref, we eliminate the risk of asynchronous React state leading to a race condition.
App.tsxtsx
import{ Pla yer,PlayerRef } from "@remotion /player";import React, { useState, useRef, useCallback } from "react";import { BufferManager } from"./BufferManager"; function App() {const playerRef = useRef<PlayerRef>(null);const [buffering, setBuffering] = useState(false);constpausedBecauseOfBuffering = useRef(false); const onBuffer = useCallback(() => {setBuffering(true);playerRef.curren t?.pause();pausedBecauseOfBuffering.current = true;}, []);const onContinue = useCallback(() => {setBuffering(false); // Play only if we paused because of bufferingif (pausedBecauseOfBuffering.current) {pausedBecauseOfBufferin g.current = false;playerRef.current?.play(); }}, []);return (<BufferManager onBuffer={onBuffer} onContinue={onContinue}><Playerref={playerRef}component={MyComp} compositionHeight={720} compositionWidth={1280}durationInFrames={200}fps={30}controls/></BufferManager>);}export default App;
App.tsxtsx
import{ Pla yer,PlayerRef } from "@remotion /player";import React, { useState, useRef, useCallback } from "react";import { BufferManager } from"./BufferManager"; function App() {const playerRef = useRef<PlayerRef>(null);const [buffering, setBuffering] = useState(false);constpausedBecauseOfBuffering = useRef(false); const onBuffer = useCallback(() => {setBuffering(true);playerRef.curren t?.pause();pausedBecauseOfBuffering.current = true;}, []);const onContinue = useCallback(() => {setBuffering(false); // Play only if we paused because of bufferingif (pausedBecauseOfBuffering.current) {pausedBecauseOfBufferin g.current = false;playerRef.current?.play(); }}, []);return (<BufferManager onBuffer={onBuffer} onContinue={onContinue}><Playerref={playerRef}component={MyComp} compositionHeight={720} compositionWidth={1280}durationInFrames={200}fps={30}controls/></BufferManager>);}export default App;
In addition to pausing the video, you can also display custom UI that will overlay the video while it is buffering. Usually, you would display a branded spinner, in this simplified example, we are showing a ⏳ emoji.
App.tsxtsx
import {Player ,RenderPoster } from "@remotion/player";import {useCallback ,useState } from "react";import {AbsoluteFill } from "remotion";functionApp () {const [buffering ,setBuffering ] =useState ();// Add this to your component rendering the <Player>constrenderPoster :RenderPoster =useCallback (() => {if (buffering ) {return (<AbsoluteFill style ={{justifyContent : "center",alignItems : "center",fontSize : 100,}}>⏳</AbsoluteFill >);}return null;}, [buffering ]);return (<Player fps ={30}component ={MyComp }compositionHeight ={720}compositionWidth ={1280}durationInFrames ={200}// Add these two props to the PlayershowPosterWhenPaused renderPoster ={renderPoster }/>);}
App.tsxtsx
import {Player ,RenderPoster } from "@remotion/player";import {useCallback ,useState } from "react";import {AbsoluteFill } from "remotion";functionApp () {const [buffering ,setBuffering ] =useState ();// Add this to your component rendering the <Player>constrenderPoster :RenderPoster =useCallback (() => {if (buffering ) {return (<AbsoluteFill style ={{justifyContent : "center",alignItems : "center",fontSize : 100,}}>⏳</AbsoluteFill >);}return null;}, [buffering ]);return (<Player fps ={30}component ={MyComp }compositionHeight ={720}compositionWidth ={1280}durationInFrames ={200}// Add these two props to the PlayershowPosterWhenPaused renderPoster ={renderPoster }/>);}