Integrations
elements
lifecycle
browser
- useAudio
- useBattery
- useBluetooth
- useBreakpoints
- useBroadcastChannel
- useBrowserLocation
- useClipboard
- useCopy
- useCssVar
- useDeviceList
- useDisplayMedia
- useDocumentEvent
- useDocumentTitle
- useDocumentVisibility
- useEventListener
- useEventSource
- useEyeDropper
- useFavicon
- useFileSystemAccess
- useFps
- useFullscreen
- useGamepad
- useGeolocation
- useMeasure
- useMediaControls
- useMediaQuery
- useMediaStream
- useMemory
- useNetwork
- useObjectUrl
- useOnline
- useOtpCredential
- usePermission
- usePictureInPicture
- usePointerLock
- usePostMessage
- useRaf
- useShare
- useSpeechRecognition
- useSpeechSynthesis
- useSticky
- useVibrate
- useVirtualKeyboard
- useWakeLock
- useWebSocket
utilities
state
- useBoolean
- useControllableState
- useCookie
- useCookies
- useCounter
- useCycleList
- useDefault
- useDisclosure
- useField
- useHash
- useList
- useLocalStorage
- useMap
- useMask
- useMergedRef
- useObject
- useOffsetPagination
- useQueue
- useRafState
- useRefState
- useSessionStorage
- useSet
- useStateHistory
- useStep
- useStorage
- useToggle
- useUrlSearchParam
- useUrlSearchParams
- useValidatedState
- useWizard
user
sensors
- useDeviceMotion
- useDeviceOrientation
- useHotkeys
- useIdle
- useInfiniteScroll
- useIntersectionObserver
- useKeyboard
- useKeyPress
- useKeysPressed
- useMouse
- useMutationObserver
- useOrientation
- usePageLeave
- useParallax
- usePerformanceObserver
- useResizeObserver
- useScroll
- useScrollIntoView
- useScrollTo
- useSwipe
- useTextSelection
- useVisibility
- useWindowEvent
- useWindowFocus
- useWindowScroll
- useWindowSize
API not supported, make sure to check for compatibility with different browsers when using this API
import {
useClickOutside,
useDeviceList,
useDisclosure,
useMediaStream
} from '@siberiacancode/reactuse';
import {
CameraIcon,
CameraOffIcon,
CheckIcon,
ChevronDownIcon,
LoaderCircleIcon,
PhoneOffIcon
} from 'lucide-react';
import { useState } from 'react';
import { cn } from '@/utils/lib';
const Demo = () => {
const [deviceId, setDeviceId] = useState<string | undefined>(undefined);
const dropdownMenu = useDisclosure();
const dropdownRef = useClickOutside<HTMLDivElement>(() => dropdownMenu.close());
const mediaStream = useMediaStream({
immediately: false,
constraints: {
video: true,
audio: false
}
});
const deviceList = useDeviceList();
const onCameraSelect = async (id: string) => {
setDeviceId(id);
dropdownMenu.close();
const constraints: MediaStreamConstraints = {
video: { deviceId: { exact: id } },
audio: false
};
if (mediaStream.active) await mediaStream.start(constraints);
};
const onCameraClick = async () => {
if (mediaStream.active) return mediaStream.stop();
const constraints: MediaStreamConstraints = {
video: { deviceId: { exact: deviceId } },
audio: false
};
const stream = await mediaStream.start(constraints);
if (!stream) return;
const [videoTrack] = stream.getVideoTracks();
const activeDeviceId = videoTrack.getSettings().deviceId;
setDeviceId(activeDeviceId);
deviceList.update();
};
if (!mediaStream.supported) {
return (
<p>
API not supported, make sure to check for compatibility with different browsers when using
this{' '}
<a
href='https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia'
rel='noreferrer'
target='_blank'
>
API
</a>
</p>
);
}
const activeCamera = deviceList.videoInputs.find((camera) => camera.deviceId === deviceId);
return (
<section className='flex w-[350px] flex-col gap-3 p-4'>
<div className='bg-card/70 relative flex flex-col rounded-xl'>
<div className='relative flex aspect-[16/9] items-center justify-center overflow-hidden p-2'>
<div className='relative size-full overflow-hidden rounded-lg'>
<video
autoPlay
muted
playsInline
ref={mediaStream.ref}
className={cn(
'absolute inset-0 size-full object-cover',
!mediaStream.active && 'hidden'
)}
/>
{!mediaStream.active && (
<div className='bg-card absolute inset-0 flex flex-col items-center justify-center gap-3 rounded-lg p-4 text-center'>
{mediaStream.loading ? (
<LoaderCircleIcon className='text-foreground size-8 animate-spin' />
) : (
<div className='flex flex-col items-center gap-3'>
<div
className='bg-muted text-foreground size-14'
data-size='lg'
data-slot='avatar'
>
<span data-slot='avatar-fallback'>YN</span>
</div>
<span className='text-foreground text-sm font-medium'>Your Name</span>
</div>
)}
</div>
)}
</div>
</div>
<div className='flex items-center justify-between gap-2 p-3'>
<div className='flex items-center gap-2'>
<button
aria-label={mediaStream.active ? 'Turn camera off' : 'Turn camera on'}
data-variant='secondary'
disabled={!deviceList.videoInputs.length}
type='button'
onClick={onCameraClick}
>
{mediaStream.active ? (
<CameraIcon className='size-4' />
) : (
<CameraOffIcon className='size-4' />
)}
</button>
<div ref={dropdownRef} className='relative'>
<button
aria-label='Select camera'
className='flex w-44 items-center gap-2'
data-variant='secondary'
type='button'
onClick={() => dropdownMenu.toggle()}
>
<span className='min-w-0 flex-1 truncate text-left text-xs'>
{activeCamera?.label || 'Default camera'}
</span>
<ChevronDownIcon className='size-4' />
</button>
{dropdownMenu.opened && (
<div
className='absolute bottom-full left-0 z-20 mb-2 w-56'
data-slot='dropdown-menu-content'
>
{!deviceList.videoInputs.length && (
<div data-slot='dropdown-menu-item'>No cameras found</div>
)}
{deviceList.videoInputs.map((camera) => (
<div
key={camera.deviceId}
data-slot='dropdown-menu-item'
onClick={() => onCameraSelect(camera.deviceId)}
>
<CameraIcon className='size-4' />
<span className='truncate'>
{camera.label || `Camera ${camera.deviceId.slice(0, 6)}`}
</span>
{camera.deviceId === deviceId && (
<CheckIcon className='size-4' data-slot='dropdown-menu-shortcut' />
)}
</div>
))}
</div>
)}
</div>
</div>
<button data-variant='destructive' type='button'>
<PhoneOffIcon className='size-4' />
End
</button>
</div>
</div>
</section>
);
};
export default Demo;
This hook uses navigator.mediaDevices.getUserMedia browser api to provide enhanced functionality. Make sure to check for compatibility with different browsers when using this api
Installation
pnpm add @siberiacancode/reactuseUsage
const { stream, start, apply, stop } = useMediaStream(videoRef);
// or
const { ref, stream, start, apply, stop } = useMediaStream<HTMLVideoElement>();Type Declarations
import type { HookTarget } from '@/utils/helpers';
import type { StateRef } from '../useRefState/useRefState';
export interface UseMediaStreamOptions {
/** Default constraints to be passed to `getUserMedia` on the first request */
constraints?: MediaStreamConstraints;
/** Whether the stream should be requested immediately */
immediately?: boolean;
/** The callback fired when requesting the stream fails */
onError?: (error: Error) => void;
/** The callback fired once the stream was successfully obtained */
onStart?: (stream: MediaStream) => void;
/** The callback fired when the stream is stopped */
onStop?: (stream?: MediaStream) => void;
}
export interface UseMediaStreamReturn {
/** Whether a stream is currently active */
active: boolean;
/** Whether a stream is currently being requested */
loading: boolean;
/** The current media stream, if any */
stream?: MediaStream;
/** Whether `mediaDevices.getUserMedia` is supported by the browser */
supported: boolean;
/** Apply constraints to the live media tracks without recreating the stream */
apply: (constraints: MediaStreamConstraints) => Promise<boolean>;
/** Stop the current stream and request a new one with the current constraints */
restart: () => Promise<MediaStream | undefined>;
/** Request and start the media stream */
start: (constraints?: MediaStreamConstraints) => Promise<MediaStream | undefined>;
/** Stop all tracks of the current media stream */
stop: () => void;
}
export interface UseMediaStream {
(target: HookTarget, options?: UseMediaStreamOptions): UseMediaStreamReturn;
(options?: UseMediaStreamOptions): UseMediaStreamReturn & {
ref: StateRef<HTMLVideoElement>;
};
}API
Parameters
| Name | Type | Default | Note |
|---|---|---|---|
| target | HookTarget | - | The target video element the stream will be attached to |
| options.immediately | boolean | false | Whether the stream should be requested immediately |
| options.constraints | MediaStreamConstraints | - | Default constraints passed to `getUserMedia` |
| options.onStart | (stream: MediaStream) => void | - | Callback fired when the stream starts |
| options.onStop | (stream?: MediaStream) => void | - | Callback fired when the stream stops |
| options.onError | (error: Error) => void | - | Callback fired when the request fails |
Returns
UseMediaStreamReturnParameters
| Name | Type | Default | Note |
|---|---|---|---|
| options.immediately | boolean | false | Whether the stream should be requested immediately |
| options.constraints | MediaStreamConstraints | - | Default constraints passed to `getUserMedia` |
| options.onStart | (stream: MediaStream) => void | - | Callback fired when the stream starts |
| options.onStop | (stream?: MediaStream) => void | - | Callback fired when the stream stops |
| options.onError | (error: Error) => void | - | Callback fired when the request fails |
Returns
UseMediaStreamReturn & { ref: StateRef<HTMLVideoElement> }Contributors
ddebabin
Last updated on