Display a buffer state
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.tsxtsximport { 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.tsxtsximport { 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.tsxtsximport {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.tsxtsximport {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.tsxtsximportReact , {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.tsxtsximportReact , {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.tsxtsximport{ 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.tsxtsximport{ 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.tsxtsximport {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.tsxtsximport {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 }/>);}