UNPKG

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