Custom controls for the Player
You may want to implement custom controls for the <Player>
component.
There are two approaches:
- Enable the
controls
prop and granunarly override some or all of the controls inside the Player. - Disable the
controls
prop 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 controls over the look and behavior of the controls
Ensure the controls
prop is not set in the <Player/>
.
Obtain a ref
of type PlayerRef
of the <Player/>
and use the following starting points to implement your own controls:
Play / Pause button
PlayPauseButton.tsxtsx
import type {PlayerRef } from '@remotion/player';importReact , {SVGProps ,useCallback ,useEffect } from 'react';export constPlayPauseButton :React .FC <{playerRef :React .RefObject <PlayerRef >;}> = ({playerRef }) => {const [playing ,setPlaying ] =React .useState (() =>playerRef .current ?.isPlaying () ?? false,);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 ]);constonToggle =useCallback (() => {playerRef .current ?.toggle ();}, [playerRef ]);return (<button onClick ={onToggle }type ="button">{playing ? 'Pause' : 'Play'}</button >);};
PlayPauseButton.tsxtsx
import type {PlayerRef } from '@remotion/player';importReact , {SVGProps ,useCallback ,useEffect } from 'react';export constPlayPauseButton :React .FC <{playerRef :React .RefObject <PlayerRef >;}> = ({playerRef }) => {const [playing ,setPlaying ] =React .useState (() =>playerRef .current ?.isPlaying () ?? false,);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 ]);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.tsxtsx
import 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 }`;};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.tsxtsx
import 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 }`;};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.tsxtsx
import type {PlayerRef } from '@remotion/player';importReact , {useCallback ,useEffect ,useState } from 'react';export constPlayerFullscreen :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.tsxtsx
import type {PlayerRef } from '@remotion/player';importReact , {useCallback ,useEffect ,useState } from 'react';export constPlayerFullscreen :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
tsx
import 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,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 constPlayerSeekBar :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 >);};
tsx
import 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,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 constPlayerSeekBar :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 >);};
Volume slider
TODO
Mute button
TODO