Custom controls for the Player
You may want to implement custom controls for the <Player> component.
There are two approaches:
- Enable the
controlsprop and granunarly override some or all of the controls inside the Player. - Disable the
controlsprop and implement your own controls anywhere on the page.
Custom inline controls
Use this approach if you:
- Like the default controls but want to customize some of them
- Want the controls to overlay the Player.
Ensure the controls prop is set in the <Player/>.
Use the following APIs to customize the individual controls:
Controls outside the Player
Use this approach if you:
- Want to implement custom controls anywhere on the page
- Want full control over the look and behavior of the controls
Use the following starting points to implement your own controls. You will need the following prerequisites:
- Ensure the
controlsprop is not set in the<Player/>. - Obtain a
refof typePlayerRefof the<Player/>. - Some of the components will require
durationInFramesandfpsprops. Place the values in shared variables to be used in both<Player/>and these components. - The
<SeekBar/>component can optionally acceptinFrameandoutFrameprops. They're the same values passed to<Player/>(also optional).
Play / Pause button
PlayPauseButton.tsxtsximport type {PlayerRef } from '@remotion/player';import {useCallback ,useEffect ,useState } from 'react';export constPlayPauseButton :React .FC <{playerRef :React .RefObject <PlayerRef >;}> = ({playerRef }) => {const [playing ,setPlaying ] =useState (false);useEffect (() => {const {current } =playerRef ;setPlaying (current ?.isPlaying () ?? false);if (!current ) return;constonPlay = () => {setPlaying (true);};constonPause = () => {setPlaying (false);};current .addEventListener ('play',onPlay );current .addEventListener ('pause',onPause );return () => {current .removeEventListener ('play',onPlay );current .removeEventListener ('pause',onPause );};}, [playerRef ]);constonToggle =useCallback (() => {playerRef .current ?.toggle ();}, [playerRef ]);return (<button onClick ={onToggle }type ="button">{playing ? 'Pause' : 'Play'}</button >);};
PlayPauseButton.tsxtsximport type {PlayerRef } from '@remotion/player';import {useCallback ,useEffect ,useState } from 'react';export constPlayPauseButton :React .FC <{playerRef :React .RefObject <PlayerRef >;}> = ({playerRef }) => {const [playing ,setPlaying ] =useState (false);useEffect (() => {const {current } =playerRef ;setPlaying (current ?.isPlaying () ?? false);if (!current ) return;constonPlay = () => {setPlaying (true);};constonPause = () => {setPlaying (false);};current .addEventListener ('play',onPlay );current .addEventListener ('pause',onPause );return () => {current .removeEventListener ('play',onPlay );current .removeEventListener ('pause',onPause );};}, [playerRef ]);constonToggle =useCallback (() => {playerRef .current ?.toggle ();}, [playerRef ]);return (<button onClick ={onToggle }type ="button">{playing ? 'Pause' : 'Play'}</button >);};
The buffering indicator is not implemented in this snippet.
Time display
TimeDisplay.tsxtsximport type {PlayerRef } from '@remotion/player';importReact , {useEffect } from 'react';export constformatTime = (frame : number,fps : number): string => {consthours =Math .floor (frame /fps / 3600);constremainingMinutes =frame -hours *fps * 3600;constminutes =Math .floor (remainingMinutes / 60 /fps );constremainingSec =frame -hours *fps * 3600 -minutes *fps * 60;constseconds =Math .floor (remainingSec /fps );constframeAfterSec =Math .round (frame %fps );consthoursStr =String (hours );constminutesStr =String (minutes ).padStart (2, '0');constsecondsStr =String (seconds ).padStart (2, '0');constframeStr =String (frameAfterSec ).padStart (2, '0');if (hours > 0) {return `${hoursStr }:${minutesStr }:${secondsStr }.${frameStr }`;}return `${minutesStr }:${secondsStr }.${frameStr }`;};export constTimeDisplay :React .FC <{durationInFrames : number;fps : number;playerRef :React .RefObject <PlayerRef >;}> = ({durationInFrames ,fps ,playerRef }) => {const [time ,setTime ] =React .useState (0);useEffect (() => {const {current } =playerRef ;if (!current ) {return;}constonTimeUpdate = () => {setTime (current .getCurrentFrame ());};current .addEventListener ('frameupdate',onTimeUpdate );return () => {current .removeEventListener ('frameupdate',onTimeUpdate );};}, [playerRef ]);return (<div style ={{fontFamily : 'monospace',}}><span >{formatTime (time ,fps )}/{formatTime (durationInFrames ,fps )}</span ></div >);};
TimeDisplay.tsxtsximport type {PlayerRef } from '@remotion/player';importReact , {useEffect } from 'react';export constformatTime = (frame : number,fps : number): string => {consthours =Math .floor (frame /fps / 3600);constremainingMinutes =frame -hours *fps * 3600;constminutes =Math .floor (remainingMinutes / 60 /fps );constremainingSec =frame -hours *fps * 3600 -minutes *fps * 60;constseconds =Math .floor (remainingSec /fps );constframeAfterSec =Math .round (frame %fps );consthoursStr =String (hours );constminutesStr =String (minutes ).padStart (2, '0');constsecondsStr =String (seconds ).padStart (2, '0');constframeStr =String (frameAfterSec ).padStart (2, '0');if (hours > 0) {return `${hoursStr }:${minutesStr }:${secondsStr }.${frameStr }`;}return `${minutesStr }:${secondsStr }.${frameStr }`;};export constTimeDisplay :React .FC <{durationInFrames : number;fps : number;playerRef :React .RefObject <PlayerRef >;}> = ({durationInFrames ,fps ,playerRef }) => {const [time ,setTime ] =React .useState (0);useEffect (() => {const {current } =playerRef ;if (!current ) {return;}constonTimeUpdate = () => {setTime (current .getCurrentFrame ());};current .addEventListener ('frameupdate',onTimeUpdate );return () => {current .removeEventListener ('frameupdate',onTimeUpdate );};}, [playerRef ]);return (<div style ={{fontFamily : 'monospace',}}><span >{formatTime (time ,fps )}/{formatTime (durationInFrames ,fps )}</span ></div >);};
The conventional time formatting for video editors is hh:mm:ss.ff where hh is hours, mm is minutes, ss is seconds and ff is frames past the second.
Fullscreen button
Pay attention to two nuances when implementing the Fullscreen button:
- Not all browsers support Fullscreen, feature detection should be performed.
- If using server-side rendering, feature detection should be performed after the component has been mounted on the client to avoid a React hydration mismatch.
FullscreenButton.tsxtsximport type {PlayerRef } from '@remotion/player';importReact , {useCallback ,useEffect ,useState } from 'react';export constFullscreenButton :React .FC <{playerRef :React .RefObject <PlayerRef >;}> = ({playerRef }) => {const [supportsFullscreen ,setSupportsFullscreen ] =useState (false);const [isFullscreen ,setIsFullscreen ] =useState (false);useEffect (() => {const {current } =playerRef ;if (!current ) {return;}constonFullscreenChange = () => {setIsFullscreen (document .fullscreenElement !== null);};current .addEventListener ('fullscreenchange',onFullscreenChange );return () => {current .removeEventListener ('fullscreenchange',onFullscreenChange );};}, [playerRef ]);useEffect (() => {// Must be handled client-side to avoid SSR hydration mismatchsetSupportsFullscreen ((typeofdocument !== 'undefined' &&(document .fullscreenEnabled ||// @ts-expect-error Types not defineddocument .webkitFullscreenEnabled )) ??false,);}, []);constonClick =useCallback (() => {const {current } =playerRef ;if (!current ) {return;}if (isFullscreen ) {current .exitFullscreen ();} else {current .requestFullscreen ();}}, [isFullscreen ,playerRef ]);if (!supportsFullscreen ) {return null;}return (<button type ="button"onClick ={onClick }>{isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}</button >);};
FullscreenButton.tsxtsximport type {PlayerRef } from '@remotion/player';importReact , {useCallback ,useEffect ,useState } from 'react';export constFullscreenButton :React .FC <{playerRef :React .RefObject <PlayerRef >;}> = ({playerRef }) => {const [supportsFullscreen ,setSupportsFullscreen ] =useState (false);const [isFullscreen ,setIsFullscreen ] =useState (false);useEffect (() => {const {current } =playerRef ;if (!current ) {return;}constonFullscreenChange = () => {setIsFullscreen (document .fullscreenElement !== null);};current .addEventListener ('fullscreenchange',onFullscreenChange );return () => {current .removeEventListener ('fullscreenchange',onFullscreenChange );};}, [playerRef ]);useEffect (() => {// Must be handled client-side to avoid SSR hydration mismatchsetSupportsFullscreen ((typeofdocument !== 'undefined' &&(document .fullscreenEnabled ||// @ts-expect-error Types not defineddocument .webkitFullscreenEnabled )) ??false,);}, []);constonClick =useCallback (() => {const {current } =playerRef ;if (!current ) {return;}if (isFullscreen ) {current .exitFullscreen ();} else {current .requestFullscreen ();}}, [isFullscreen ,playerRef ]);if (!supportsFullscreen ) {return null;}return (<button type ="button"onClick ={onClick }>{isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}</button >);};
The Exit Fullscreen label is hypothetical since if it is rendered outside of the Player, it would not be visible while in Fullscreen.
Seek bar
SeekBar.tsxtsximport type {PlayerRef } from '@remotion/player';importReact , {useCallback ,useEffect ,useMemo ,useRef ,useState } from 'react';import {interpolate } from 'remotion';typeSize = {width : number;height : number;left : number;top : number;};// If a pane has been moved, it will cause a layout shift without// the window having been resized. Those UI elements can call this API to// force an updateexport constuseElementSize = (ref :React .RefObject <HTMLElement >,):Size | null => {const [size ,setSize ] =useState <Size | null>(() => {if (!ref .current ) {return null;}constrect =ref .current .getClientRects ();if (!rect [0]) {return null;}return {width :rect [0].width as number,height :rect [0].height as number,left :rect [0].x as number,top :rect [0].y as number,};});constobserver =useMemo (() => {if (typeofResizeObserver === 'undefined') {return null;}return newResizeObserver ((entries ) => {const {target } =entries [0];constnewSize =target .getClientRects ();if (!newSize ?.[0]) {setSize (null);return;}const {width } =newSize [0];const {height } =newSize [0];setSize ({width ,height ,left :newSize [0].x ,top :newSize [0].y ,});});}, []);constupdateSize =useCallback (() => {if (!ref .current ) {return;}constrect =ref .current .getClientRects ();if (!rect [0]) {setSize (null);return;}setSize ((prevState ) => {constisSame =prevState &&prevState .width ===rect [0].width &&prevState .height ===rect [0].height &&prevState .left ===rect [0].x &&prevState .top ===rect [0].y ;if (isSame ) {returnprevState ;}return {width :rect [0].width as number,height :rect [0].height as number,left :rect [0].x as number,top :rect [0].y as number,windowSize : {height :window .innerHeight ,width :window .innerWidth ,},};});}, [ref ]);useEffect (() => {if (!observer ) {return;}const {current } =ref ;if (current ) {observer .observe (current );}return (): void => {if (current ) {observer .unobserve (current );}};}, [observer ,ref ,updateSize ]);useEffect (() => {window .addEventListener ('resize',updateSize );return () => {window .removeEventListener ('resize',updateSize );};}, [updateSize ]);returnuseMemo (() => {if (!size ) {return null;}return {...size ,refresh :updateSize };}, [size ,updateSize ]);};constgetFrameFromX = (clientX : number,durationInFrames : number,width : number,) => {constpos =clientX ;constframe =Math .round (interpolate (pos , [0,width ], [0,Math .max (durationInFrames - 1, 0)], {extrapolateLeft : 'clamp',extrapolateRight : 'clamp',}),);returnframe ;};constBAR_HEIGHT = 5;constKNOB_SIZE = 12;constVERTICAL_PADDING = 4;constcontainerStyle :React .CSSProperties = {userSelect : 'none',WebkitUserSelect : 'none',paddingTop :VERTICAL_PADDING ,paddingBottom :VERTICAL_PADDING ,boxSizing : 'border-box',cursor : 'pointer',position : 'relative',touchAction : 'none',flex : 1,};constbarBackground :React .CSSProperties = {height :BAR_HEIGHT ,backgroundColor : 'rgba(0, 0, 0, 0.25)',width : '100%',borderRadius :BAR_HEIGHT / 2,};constfindBodyInWhichDivIsLocated = (div :HTMLElement ) => {letcurrent =div ;while (current .parentElement ) {current =current .parentElement ;}returncurrent ;};export constuseHoverState = (ref :React .RefObject <HTMLDivElement >) => {const [hovered ,setHovered ] =useState (false);useEffect (() => {const {current } =ref ;if (!current ) {return;}constonHover = () => {setHovered (true);};constonLeave = () => {setHovered (false);};constonMove = () => {setHovered (true);};current .addEventListener ('mouseenter',onHover );current .addEventListener ('mouseleave',onLeave );current .addEventListener ('mousemove',onMove );return () => {current .removeEventListener ('mouseenter',onHover );current .removeEventListener ('mouseleave',onLeave );current .removeEventListener ('mousemove',onMove );};}, [ref ]);returnhovered ;};export constSeekBar :React .FC <{durationInFrames : number;inFrame ?: number | null;outFrame ?: number | null;playerRef :React .RefObject <PlayerRef >;}> = ({durationInFrames ,inFrame ,outFrame ,playerRef }) => {constcontainerRef =useRef <HTMLDivElement >(null);constbarHovered =useHoverState (containerRef );constsize =useElementSize (containerRef );const [playing ,setPlaying ] =useState (false);const [frame ,setFrame ] =useState (0);useEffect (() => {const {current } =playerRef ;if (!current ) {return;}constonFrameUpdate = () => {setFrame (current .getCurrentFrame ());};current .addEventListener ('frameupdate',onFrameUpdate );return () => {current .removeEventListener ('frameupdate',onFrameUpdate );};}, [playerRef ]);useEffect (() => {const {current } =playerRef ;if (!current ) {return;}constonPlay = () => {setPlaying (true);};constonPause = () => {setPlaying (false);};current .addEventListener ('play',onPlay );current .addEventListener ('pause',onPause );return () => {current .removeEventListener ('play',onPlay );current .removeEventListener ('pause',onPause );};}, [playerRef ]);const [dragging ,setDragging ] =useState <| {dragging : false;}| {dragging : true;wasPlaying : boolean;}>({dragging : false,});constwidth =size ?.width ?? 0;constonPointerDown =useCallback ((e :React .PointerEvent <HTMLDivElement >) => {if (e .button !== 0) {return;}if (!playerRef .current ) {return;}constposLeft =containerRef .current ?.getBoundingClientRect ().left as number;const_frame =getFrameFromX (e .clientX -posLeft ,durationInFrames ,width ,);playerRef .current .pause ();playerRef .current .seekTo (_frame );setDragging ({dragging : true,wasPlaying :playing ,});},[durationInFrames ,width ,playerRef ,playing ],);constonPointerMove =useCallback ((e :PointerEvent ) => {if (!size ) {throw newError ('Player has no size');}if (!dragging .dragging ) {return;}if (!playerRef .current ) {return;}constposLeft =containerRef .current ?.getBoundingClientRect ().left as number;const_frame =getFrameFromX (e .clientX -posLeft ,durationInFrames ,size .width ,);playerRef .current .seekTo (_frame );},[dragging .dragging ,durationInFrames ,playerRef ,size ],);constonPointerUp =useCallback (() => {setDragging ({dragging : false,});if (!dragging .dragging ) {return;}if (!playerRef .current ) {return;}if (dragging .wasPlaying ) {playerRef .current .play ();} else {playerRef .current .pause ();}}, [dragging ,playerRef ]);useEffect (() => {if (!dragging .dragging ) {return;}constbody =findBodyInWhichDivIsLocated (containerRef .current asHTMLElement ,);body .addEventListener ('pointermove',onPointerMove );body .addEventListener ('pointerup',onPointerUp );return () => {body .removeEventListener ('pointermove',onPointerMove );body .removeEventListener ('pointerup',onPointerUp );};}, [dragging .dragging ,onPointerMove ,onPointerUp ]);constknobStyle :React .CSSProperties =useMemo (() => {return {height :KNOB_SIZE ,width :KNOB_SIZE ,borderRadius :KNOB_SIZE / 2,position : 'absolute',top :VERTICAL_PADDING -KNOB_SIZE / 2 + 5 / 2,backgroundColor : '#000',left :Math .max (0,(frame /Math .max (1,durationInFrames - 1)) *width -KNOB_SIZE / 2,),boxShadow : '0 0 2px black',opacity :Number (barHovered ),transition : 'opacity 0.1s ease',};}, [barHovered ,durationInFrames ,frame ,width ]);constfillStyle :React .CSSProperties =useMemo (() => {return {height :BAR_HEIGHT ,backgroundColor : '#000',width : ((frame - (inFrame ?? 0)) / (durationInFrames - 1)) * 100 + '%',marginLeft : ((inFrame ?? 0) / (durationInFrames - 1)) * 100 + '%',borderRadius :BAR_HEIGHT / 2,};}, [durationInFrames ,frame ,inFrame ]);constactive :React .CSSProperties =useMemo (() => {return {height :BAR_HEIGHT ,backgroundColor : '#000',opacity : 0.6,width :(((outFrame ??durationInFrames - 1) - (inFrame ?? 0)) /(durationInFrames - 1)) *100 +'%',marginLeft : ((inFrame ?? 0) / (durationInFrames - 1)) * 100 + '%',borderRadius :BAR_HEIGHT / 2,position : 'absolute',};}, [durationInFrames ,inFrame ,outFrame ]);return (<div ref ={containerRef }onPointerDown ={onPointerDown }style ={containerStyle }><div style ={barBackground }><div style ={active } /><div style ={fillStyle } /></div ><div style ={knobStyle } /></div >);};
SeekBar.tsxtsximport type {PlayerRef } from '@remotion/player';importReact , {useCallback ,useEffect ,useMemo ,useRef ,useState } from 'react';import {interpolate } from 'remotion';typeSize = {width : number;height : number;left : number;top : number;};// If a pane has been moved, it will cause a layout shift without// the window having been resized. Those UI elements can call this API to// force an updateexport constuseElementSize = (ref :React .RefObject <HTMLElement >,):Size | null => {const [size ,setSize ] =useState <Size | null>(() => {if (!ref .current ) {return null;}constrect =ref .current .getClientRects ();if (!rect [0]) {return null;}return {width :rect [0].width as number,height :rect [0].height as number,left :rect [0].x as number,top :rect [0].y as number,};});constobserver =useMemo (() => {if (typeofResizeObserver === 'undefined') {return null;}return newResizeObserver ((entries ) => {const {target } =entries [0];constnewSize =target .getClientRects ();if (!newSize ?.[0]) {setSize (null);return;}const {width } =newSize [0];const {height } =newSize [0];setSize ({width ,height ,left :newSize [0].x ,top :newSize [0].y ,});});}, []);constupdateSize =useCallback (() => {if (!ref .current ) {return;}constrect =ref .current .getClientRects ();if (!rect [0]) {setSize (null);return;}setSize ((prevState ) => {constisSame =prevState &&prevState .width ===rect [0].width &&prevState .height ===rect [0].height &&prevState .left ===rect [0].x &&prevState .top ===rect [0].y ;if (isSame ) {returnprevState ;}return {width :rect [0].width as number,height :rect [0].height as number,left :rect [0].x as number,top :rect [0].y as number,windowSize : {height :window .innerHeight ,width :window .innerWidth ,},};});}, [ref ]);useEffect (() => {if (!observer ) {return;}const {current } =ref ;if (current ) {observer .observe (current );}return (): void => {if (current ) {observer .unobserve (current );}};}, [observer ,ref ,updateSize ]);useEffect (() => {window .addEventListener ('resize',updateSize );return () => {window .removeEventListener ('resize',updateSize );};}, [updateSize ]);returnuseMemo (() => {if (!size ) {return null;}return {...size ,refresh :updateSize };}, [size ,updateSize ]);};constgetFrameFromX = (clientX : number,durationInFrames : number,width : number,) => {constpos =clientX ;constframe =Math .round (interpolate (pos , [0,width ], [0,Math .max (durationInFrames - 1, 0)], {extrapolateLeft : 'clamp',extrapolateRight : 'clamp',}),);returnframe ;};constBAR_HEIGHT = 5;constKNOB_SIZE = 12;constVERTICAL_PADDING = 4;constcontainerStyle :React .CSSProperties = {userSelect : 'none',WebkitUserSelect : 'none',paddingTop :VERTICAL_PADDING ,paddingBottom :VERTICAL_PADDING ,boxSizing : 'border-box',cursor : 'pointer',position : 'relative',touchAction : 'none',flex : 1,};constbarBackground :React .CSSProperties = {height :BAR_HEIGHT ,backgroundColor : 'rgba(0, 0, 0, 0.25)',width : '100%',borderRadius :BAR_HEIGHT / 2,};constfindBodyInWhichDivIsLocated = (div :HTMLElement ) => {letcurrent =div ;while (current .parentElement ) {current =current .parentElement ;}returncurrent ;};export constuseHoverState = (ref :React .RefObject <HTMLDivElement >) => {const [hovered ,setHovered ] =useState (false);useEffect (() => {const {current } =ref ;if (!current ) {return;}constonHover = () => {setHovered (true);};constonLeave = () => {setHovered (false);};constonMove = () => {setHovered (true);};current .addEventListener ('mouseenter',onHover );current .addEventListener ('mouseleave',onLeave );current .addEventListener ('mousemove',onMove );return () => {current .removeEventListener ('mouseenter',onHover );current .removeEventListener ('mouseleave',onLeave );current .removeEventListener ('mousemove',onMove );};}, [ref ]);returnhovered ;};export constSeekBar :React .FC <{durationInFrames : number;inFrame ?: number | null;outFrame ?: number | null;playerRef :React .RefObject <PlayerRef >;}> = ({durationInFrames ,inFrame ,outFrame ,playerRef }) => {constcontainerRef =useRef <HTMLDivElement >(null);constbarHovered =useHoverState (containerRef );constsize =useElementSize (containerRef );const [playing ,setPlaying ] =useState (false);const [frame ,setFrame ] =useState (0);useEffect (() => {const {current } =playerRef ;if (!current ) {return;}constonFrameUpdate = () => {setFrame (current .getCurrentFrame ());};current .addEventListener ('frameupdate',onFrameUpdate );return () => {current .removeEventListener ('frameupdate',onFrameUpdate );};}, [playerRef ]);useEffect (() => {const {current } =playerRef ;if (!current ) {return;}constonPlay = () => {setPlaying (true);};constonPause = () => {setPlaying (false);};current .addEventListener ('play',onPlay );current .addEventListener ('pause',onPause );return () => {current .removeEventListener ('play',onPlay );current .removeEventListener ('pause',onPause );};}, [playerRef ]);const [dragging ,setDragging ] =useState <| {dragging : false;}| {dragging : true;wasPlaying : boolean;}>({dragging : false,});constwidth =size ?.width ?? 0;constonPointerDown =useCallback ((e :React .PointerEvent <HTMLDivElement >) => {if (e .button !== 0) {return;}if (!playerRef .current ) {return;}constposLeft =containerRef .current ?.getBoundingClientRect ().left as number;const_frame =getFrameFromX (e .clientX -posLeft ,durationInFrames ,width ,);playerRef .current .pause ();playerRef .current .seekTo (_frame );setDragging ({dragging : true,wasPlaying :playing ,});},[durationInFrames ,width ,playerRef ,playing ],);constonPointerMove =useCallback ((e :PointerEvent ) => {if (!size ) {throw newError ('Player has no size');}if (!dragging .dragging ) {return;}if (!playerRef .current ) {return;}constposLeft =containerRef .current ?.getBoundingClientRect ().left as number;const_frame =getFrameFromX (e .clientX -posLeft ,durationInFrames ,size .width ,);playerRef .current .seekTo (_frame );},[dragging .dragging ,durationInFrames ,playerRef ,size ],);constonPointerUp =useCallback (() => {setDragging ({dragging : false,});if (!dragging .dragging ) {return;}if (!playerRef .current ) {return;}if (dragging .wasPlaying ) {playerRef .current .play ();} else {playerRef .current .pause ();}}, [dragging ,playerRef ]);useEffect (() => {if (!dragging .dragging ) {return;}constbody =findBodyInWhichDivIsLocated (containerRef .current asHTMLElement ,);body .addEventListener ('pointermove',onPointerMove );body .addEventListener ('pointerup',onPointerUp );return () => {body .removeEventListener ('pointermove',onPointerMove );body .removeEventListener ('pointerup',onPointerUp );};}, [dragging .dragging ,onPointerMove ,onPointerUp ]);constknobStyle :React .CSSProperties =useMemo (() => {return {height :KNOB_SIZE ,width :KNOB_SIZE ,borderRadius :KNOB_SIZE / 2,position : 'absolute',top :VERTICAL_PADDING -KNOB_SIZE / 2 + 5 / 2,backgroundColor : '#000',left :Math .max (0,(frame /Math .max (1,durationInFrames - 1)) *width -KNOB_SIZE / 2,),boxShadow : '0 0 2px black',opacity :Number (barHovered ),transition : 'opacity 0.1s ease',};}, [barHovered ,durationInFrames ,frame ,width ]);constfillStyle :React .CSSProperties =useMemo (() => {return {height :BAR_HEIGHT ,backgroundColor : '#000',width : ((frame - (inFrame ?? 0)) / (durationInFrames - 1)) * 100 + '%',marginLeft : ((inFrame ?? 0) / (durationInFrames - 1)) * 100 + '%',borderRadius :BAR_HEIGHT / 2,};}, [durationInFrames ,frame ,inFrame ]);constactive :React .CSSProperties =useMemo (() => {return {height :BAR_HEIGHT ,backgroundColor : '#000',opacity : 0.6,width :(((outFrame ??durationInFrames - 1) - (inFrame ?? 0)) /(durationInFrames - 1)) *100 +'%',marginLeft : ((inFrame ?? 0) / (durationInFrames - 1)) * 100 + '%',borderRadius :BAR_HEIGHT / 2,position : 'absolute',};}, [durationInFrames ,inFrame ,outFrame ]);return (<div ref ={containerRef }onPointerDown ={onPointerDown }style ={containerStyle }><div style ={barBackground }><div style ={active } /><div style ={fillStyle } /></div ><div style ={knobStyle } /></div >);};
Loop button
loop is a prop of the <Player/> component, so you can just control is using a useState hook.
LoopButton.tsxtsximportReact from 'react';export constLoopButton :React .FC <{loop : boolean;setLoop :React .Dispatch <React .SetStateAction <boolean>>;}> = ({loop ,setLoop }) => {constonClick =React .useCallback (() => {setLoop ((prev ) => !prev );}, [setLoop ]);return (<button type ="button"onClick ={onClick }>{loop ? 'Loop enabled' : 'Loop disabled'}</button >);};
LoopButton.tsxtsximportReact from 'react';export constLoopButton :React .FC <{loop : boolean;setLoop :React .Dispatch <React .SetStateAction <boolean>>;}> = ({loop ,setLoop }) => {constonClick =React .useCallback (() => {setLoop ((prev ) => !prev );}, [setLoop ]);return (<button type ="button"onClick ={onClick }>{loop ? 'Loop enabled' : 'Loop disabled'}</button >);};
UsagetsximportReact , {useState } from 'react';import {LoopButton } from './LoopButton';import {Player } from '@remotion/player';export constMyComponent :React .FC = () => {const [loop ,setLoop ] =useState (false);return (<><Player component ={MyComp }loop ={loop }durationInFrames ={100}fps ={30}compositionWidth ={1920}compositionHeight ={1080}inputProps ={{}}/><LoopButton loop ={loop }setLoop ={setLoop } /></>);};
UsagetsximportReact , {useState } from 'react';import {LoopButton } from './LoopButton';import {Player } from '@remotion/player';export constMyComponent :React .FC = () => {const [loop ,setLoop ] =useState (false);return (<><Player component ={MyComp }loop ={loop }durationInFrames ={100}fps ={30}compositionWidth ={1920}compositionHeight ={1080}inputProps ={{}}/><LoopButton loop ={loop }setLoop ={setLoop } /></>);};
Volume slider
Note that if the video is "muted", the volume state may be greater than 0.
The following component handles the special case of the video being "muted":
- If the video is muted, set the slider value to 0.
- If the slider is being slided, unmute the video if necessary.
This allows us to keep an internal state of the volume that was set before muting the video and reset the slider to that value after unmuting.
VolumeSlider.tsxtsximport type {PlayerRef } from '@remotion/player';importReact , {useEffect ,useState } from 'react';export constVolumeSlider :React .FC <{playerRef :React .RefObject <PlayerRef >;}> = ({playerRef }) => {const [volume ,setVolume ] =useState (playerRef .current ?.getVolume () ?? 1);const [muted ,setMuted ] =useState (playerRef .current ?.isMuted () ?? false);useEffect (() => {const {current } =playerRef ;if (!current ) {return;}constonVolumeChange = () => {setVolume (current .getVolume ());};constonMuteChange = () => {setMuted (current .isMuted ());};current .addEventListener ('volumechange',onVolumeChange );current .addEventListener ('mutechange',onMuteChange );return () => {current .removeEventListener ('volumechange',onVolumeChange );current .removeEventListener ('mutechange',onMuteChange );};}, [playerRef ]);constonChange :React .ChangeEventHandler <HTMLInputElement > =React .useCallback ((evt ) => {if (!playerRef .current ) {return;}constnewVolume =Number (evt .target .value );if (newVolume > 0 &&playerRef .current .isMuted ()) {playerRef .current .unmute ();}playerRef .current .setVolume (newVolume );},[playerRef ],);return (<input value ={muted ? 0 :volume }type ="range"min ={0}max ={1}step ={0.01}onChange ={onChange }/>);};
VolumeSlider.tsxtsximport type {PlayerRef } from '@remotion/player';importReact , {useEffect ,useState } from 'react';export constVolumeSlider :React .FC <{playerRef :React .RefObject <PlayerRef >;}> = ({playerRef }) => {const [volume ,setVolume ] =useState (playerRef .current ?.getVolume () ?? 1);const [muted ,setMuted ] =useState (playerRef .current ?.isMuted () ?? false);useEffect (() => {const {current } =playerRef ;if (!current ) {return;}constonVolumeChange = () => {setVolume (current .getVolume ());};constonMuteChange = () => {setMuted (current .isMuted ());};current .addEventListener ('volumechange',onVolumeChange );current .addEventListener ('mutechange',onMuteChange );return () => {current .removeEventListener ('volumechange',onVolumeChange );current .removeEventListener ('mutechange',onMuteChange );};}, [playerRef ]);constonChange :React .ChangeEventHandler <HTMLInputElement > =React .useCallback ((evt ) => {if (!playerRef .current ) {return;}constnewVolume =Number (evt .target .value );if (newVolume > 0 &&playerRef .current .isMuted ()) {playerRef .current .unmute ();}playerRef .current .setVolume (newVolume );},[playerRef ],);return (<input value ={muted ? 0 :volume }type ="range"min ={0}max ={1}step ={0.01}onChange ={onChange }/>);};
Mute button
Remotion also considers a video "muted" if the volume is 0.
You don't need to handle a special case here.
MuteButton.tsxtsximport type {PlayerRef } from '@remotion/player';importReact , {useEffect ,useState } from 'react';export constMuteButton :React .FC <{playerRef :React .RefObject <PlayerRef >;}> = ({playerRef }) => {const [muted ,setMuted ] =useState (playerRef .current ?.isMuted () ?? false);constonClick =React .useCallback (() => {if (!playerRef .current ) {return;}if (playerRef .current .isMuted ()) {playerRef .current .unmute ();} else {playerRef .current .mute ();}}, [playerRef ]);useEffect (() => {const {current } =playerRef ;if (!current ) {return;}constonMuteChange = () => {setMuted (current .isMuted ());};current .addEventListener ('mutechange',onMuteChange );return () => {current .removeEventListener ('mutechange',onMuteChange );};}, [playerRef ]);return (<button type ="button"onClick ={onClick }>{muted ? 'Unmute' : 'Mute'}</button >);};
MuteButton.tsxtsximport type {PlayerRef } from '@remotion/player';importReact , {useEffect ,useState } from 'react';export constMuteButton :React .FC <{playerRef :React .RefObject <PlayerRef >;}> = ({playerRef }) => {const [muted ,setMuted ] =useState (playerRef .current ?.isMuted () ?? false);constonClick =React .useCallback (() => {if (!playerRef .current ) {return;}if (playerRef .current .isMuted ()) {playerRef .current .unmute ();} else {playerRef .current .mute ();}}, [playerRef ]);useEffect (() => {const {current } =playerRef ;if (!current ) {return;}constonMuteChange = () => {setMuted (current .isMuted ());};current .addEventListener ('mutechange',onMuteChange );return () => {current .removeEventListener ('mutechange',onMuteChange );};}, [playerRef ]);return (<button type ="button"onClick ={onClick }>{muted ? 'Unmute' : 'Mute'}</button >);};