Audio

A video component.

Usage

AudioExample.tsx
  1. 1

Here's the <Audio /> component in action.

Usage Notes

Be careful, while an image retains its width and height attributes after it loads, a video will override these attributes with its own dimensions once it loads.

If the width and height attributes are not the same as the video's dimensions, this will cause a layout shift. This is why the video component sets the width and height attributes of the video element to the provided values while the video is loading, and then removes them once the video is ready.

Make sure to either set the width and height attributes to the video's original resolution or style it with custom dimensions (e.g. w-[800px] h-[450px]). It's usually a good idea to respect the video's original ratio, otherwise you'll get windowboxing.

Features

Source

audio.tsx
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
import { forwardRef, useEffect, useImperativeHandle, useRef, useState, type VideoHTMLAttributes } from 'react'
import { cn } from '#app/utils/tailwind-merge'
export type VideoProps = VideoHTMLAttributes<HTMLVideoElement> & {
src: string
width: string
}
/* Re-do this component - it's just here so the audio route doesn't error out */
const Video = forwardRef<HTMLVideoElement, VideoProps>(({ src, width, height, className, ...props }, ref) => {
const [state, setState] = useState<'loading' | 'ready' | 'failed'>('loading')
const videoRef = useRef<HTMLVideoElement | null>(null)
useImperativeHandle<HTMLVideoElement | null, HTMLVideoElement | null>(ref, () => videoRef.current) // Merge the two refs
useEffect(() => {
if (videoRef.current) {
if (videoRef.current.networkState === HTMLMediaElement.NETWORK_NO_SOURCE || videoRef.current.error) {
onError()
}
}
}, [videoRef])
const onReady = () => {
setState('ready')
}
const onError = () => {
setState('failed')
}
return (
<video
ref={videoRef}
src={src}
width={width}
height={height}
onCanPlay={onReady}
onError={onError}
data-loading={state === 'loading' || undefined}
data-ready={state === 'ready' || undefined}
data-failed={state === 'failed' || undefined}
{...props}
className={cn('video', className)}
/>
)
})
Video.displayName = 'Video'
export { Video }