1/* eslint-env browser */ 2import * as React from 'react'; 3import * as Utils from './WebCameraUtils'; 4import { FacingModeToCameraType } from './WebConstants'; 5const VALID_SETTINGS_KEYS = [ 6 'autoFocus', 7 'flashMode', 8 'exposureCompensation', 9 'colorTemperature', 10 'iso', 11 'brightness', 12 'contrast', 13 'saturation', 14 'sharpness', 15 'focusDistance', 16 'whiteBalance', 17 'zoom', 18]; 19function useLoadedVideo(video, onLoaded) { 20 React.useEffect(() => { 21 if (video) { 22 video.addEventListener('loadedmetadata', () => { 23 // without this async block the constraints aren't properly applied to the camera, 24 // this means that if you were to turn on the torch and swap to the front camera, 25 // then swap back to the rear camera the torch setting wouldn't be applied. 26 requestAnimationFrame(() => { 27 onLoaded(); 28 }); 29 }); 30 } 31 }, [video]); 32} 33export function useWebCameraStream(video, preferredType, settings, { onCameraReady, onMountError, }) { 34 const isStartingCamera = React.useRef(false); 35 const activeStreams = React.useRef([]); 36 const capabilities = React.useRef({ 37 autoFocus: 'continuous', 38 flashMode: 'off', 39 whiteBalance: 'continuous', 40 zoom: 1, 41 }); 42 const [stream, setStream] = React.useState(null); 43 const mediaTrackSettings = React.useMemo(() => { 44 return stream ? stream.getTracks()[0].getSettings() : null; 45 }, [stream]); 46 // The actual camera type - this can be different from the incoming camera type. 47 const type = React.useMemo(() => { 48 if (!mediaTrackSettings) { 49 return null; 50 } 51 // On desktop no value will be returned, in this case we should assume the cameraType is 'front' 52 const { facingMode = 'user' } = mediaTrackSettings; 53 return FacingModeToCameraType[facingMode]; 54 }, [mediaTrackSettings]); 55 const getStreamDeviceAsync = React.useCallback(async () => { 56 try { 57 return await Utils.getPreferredStreamDevice(preferredType); 58 } 59 catch (nativeEvent) { 60 if (__DEV__) { 61 console.warn(`Error requesting UserMedia for type "${preferredType}":`, nativeEvent); 62 } 63 if (onMountError) { 64 onMountError({ nativeEvent }); 65 } 66 return null; 67 } 68 }, [preferredType, onMountError]); 69 const resumeAsync = React.useCallback(async () => { 70 const nextStream = await getStreamDeviceAsync(); 71 if (Utils.compareStreams(nextStream, stream)) { 72 // Do nothing if the streams are the same. 73 // This happens when the device only supports one camera (i.e. desktop) and the mode was toggled between front/back while already active. 74 // Without this check there is a screen flash while the video switches. 75 return false; 76 } 77 // Save a history of all active streams (usually 2+) so we can close them later. 78 // Keeping them open makes swapping camera types much faster. 79 if (!activeStreams.current.some((value) => value.id === nextStream?.id)) { 80 activeStreams.current.push(nextStream); 81 } 82 // Set the new stream -> update the video, settings, and actual camera type. 83 setStream(nextStream); 84 if (onCameraReady) { 85 onCameraReady(); 86 } 87 return false; 88 }, [getStreamDeviceAsync, setStream, onCameraReady, stream, activeStreams.current]); 89 React.useEffect(() => { 90 // Restart the camera and guard concurrent actions. 91 if (isStartingCamera.current) { 92 return; 93 } 94 isStartingCamera.current = true; 95 resumeAsync() 96 .then((isStarting) => { 97 isStartingCamera.current = isStarting; 98 }) 99 .catch(() => { 100 // ensure the camera can be started again. 101 isStartingCamera.current = false; 102 }); 103 }, [preferredType]); 104 // Update the native camera with any custom capabilities. 105 React.useEffect(() => { 106 const changes = {}; 107 for (const key of Object.keys(settings)) { 108 if (!VALID_SETTINGS_KEYS.includes(key)) { 109 continue; 110 } 111 const nextValue = settings[key]; 112 if (nextValue !== capabilities.current[key]) { 113 changes[key] = nextValue; 114 } 115 } 116 // Only update the native camera if changes were found 117 const hasChanges = !!Object.keys(changes).length; 118 const nextWebCameraSettings = { ...capabilities.current, ...changes }; 119 if (hasChanges) { 120 Utils.syncTrackCapabilities(preferredType, stream, changes); 121 } 122 capabilities.current = nextWebCameraSettings; 123 }, [ 124 settings.autoFocus, 125 settings.flashMode, 126 settings.exposureCompensation, 127 settings.colorTemperature, 128 settings.iso, 129 settings.brightness, 130 settings.contrast, 131 settings.saturation, 132 settings.sharpness, 133 settings.focusDistance, 134 settings.whiteBalance, 135 settings.zoom, 136 ]); 137 React.useEffect(() => { 138 // set or unset the video source. 139 if (!video.current) { 140 return; 141 } 142 Utils.setVideoSource(video.current, stream); 143 }, [video.current, stream]); 144 React.useEffect(() => { 145 return () => { 146 // Clean up on dismount, this is important for making sure the camera light goes off when the component is removed. 147 for (const stream of activeStreams.current) { 148 // Close all open streams. 149 Utils.stopMediaStream(stream); 150 } 151 if (video.current) { 152 // Invalidate the video source. 153 Utils.setVideoSource(video.current, stream); 154 } 155 }; 156 }, []); 157 // Update props when the video loads. 158 useLoadedVideo(video.current, () => { 159 Utils.syncTrackCapabilities(preferredType, stream, capabilities.current); 160 }); 161 return { 162 type, 163 mediaTrackSettings, 164 }; 165} 166//# sourceMappingURL=useWebCameraStream.js.map