1 |
|
2 | import * as React from 'react';
|
3 |
|
4 | import {
|
5 | CameraReadyListener,
|
6 | CameraType,
|
7 | MountErrorListener,
|
8 | WebCameraSettings,
|
9 | } from './Camera.types';
|
10 | import * as Utils from './WebCameraUtils';
|
11 | import { FacingModeToCameraType } from './WebConstants';
|
12 |
|
13 | const 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 |
|
28 | function useLoadedVideo(video: HTMLVideoElement | null, onLoaded: () => void) {
|
29 | React.useEffect(() => {
|
30 | if (video) {
|
31 | video.addEventListener('loadedmetadata', () => {
|
32 |
|
33 |
|
34 |
|
35 | requestAnimationFrame(() => {
|
36 | onLoaded();
|
37 | });
|
38 | });
|
39 | }
|
40 | }, [video]);
|
41 | }
|
42 |
|
43 | export 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 |
|
70 | const type = React.useMemo(() => {
|
71 | if (!mediaTrackSettings) {
|
72 | return null;
|
73 | }
|
74 |
|
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 |
|
97 |
|
98 |
|
99 | return false;
|
100 | }
|
101 |
|
102 |
|
103 |
|
104 | if (!activeStreams.current.some(value => value.id === nextStream?.id)) {
|
105 | activeStreams.current.push(nextStream!);
|
106 | }
|
107 |
|
108 |
|
109 | setStream(nextStream);
|
110 | if (onCameraReady) {
|
111 | onCameraReady();
|
112 | }
|
113 | return false;
|
114 | }, [getStreamDeviceAsync, setStream, onCameraReady, stream, activeStreams.current]);
|
115 |
|
116 | React.useEffect(() => {
|
117 |
|
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 |
|
129 | isStartingCamera.current = false;
|
130 | });
|
131 | }, [preferredType]);
|
132 |
|
133 |
|
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 |
|
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 |
|
173 | if (!video.current) {
|
174 | return;
|
175 | }
|
176 | Utils.setVideoSource(video.current, stream);
|
177 | }, [video.current, stream]);
|
178 |
|
179 | React.useEffect(() => {
|
180 | return () => {
|
181 |
|
182 | for (const stream of activeStreams.current) {
|
183 |
|
184 | Utils.stopMediaStream(stream);
|
185 | }
|
186 | if (video.current) {
|
187 |
|
188 | Utils.setVideoSource(video.current, stream);
|
189 | }
|
190 | };
|
191 | }, []);
|
192 |
|
193 |
|
194 | useLoadedVideo(video.current, () => {
|
195 | Utils.syncTrackCapabilities(preferredType, stream, capabilities.current);
|
196 | });
|
197 |
|
198 | return {
|
199 | type,
|
200 | mediaTrackSettings,
|
201 | };
|
202 | }
|