1
- import React , { forwardRef , useCallback , useImperativeHandle , useLayoutEffect , useMemo , useRef , useState } from 'react' ;
1
+ import React , { forwardRef , useImperativeHandle , useLayoutEffect , useMemo , useRef , useState } from 'react' ;
2
2
import createElement from 'react-native-web/dist/exports/createElement' ;
3
- import adapter from 'webrtc-adapter' ;
4
3
import PropTypes from 'prop-types' ;
5
4
6
5
import { Text , useWindowDimensions , View } from 'react-native' ;
7
6
import { utils } from '@monkvision/toolkit' ;
8
7
8
+ import { findDevices , findBestCandidate , setVideoSource } from './utils' ;
9
+ import getOS from '../../utils/getOS' ;
9
10
import styles from './styles' ;
10
11
11
12
const { getSize } = utils . styles ;
12
13
13
- const Video = React . forwardRef ( ( props , ref ) => createElement ( 'video' , { ...props , ref } ) ) ;
14
-
15
- const tests = [
16
- // {
17
- // label: '4K(UHD) 4:3',
18
- // width: 3840,
19
- // height: 2880,
20
- // ratio: '4:3',
21
- // }, {
22
- // label: '4K(UHD) 16:9',
23
- // width: 3840,
24
- // height: 2160,
25
- // ratio: '16:9',
26
- // },
27
- {
28
- label : 'FHD 4:3' ,
29
- width : 1920 ,
30
- height : 1440 ,
31
- ratio : '4:3' ,
32
- } , {
33
- label : 'FHD 16:9' ,
34
- width : 1920 ,
35
- height : 1080 ,
36
- ratio : '16:9' ,
37
- } , {
38
- label : 'UXGA' ,
39
- width : 1600 ,
40
- height : 1200 ,
41
- ratio : '4:3' ,
42
- } , {
43
- label : 'HD(720p)' ,
44
- width : 1280 ,
45
- height : 720 ,
46
- ratio : '16:9' ,
47
- } , {
48
- label : 'SVGA' ,
49
- width : 800 ,
50
- height : 600 ,
51
- ratio : '4:3' ,
52
- } , {
53
- label : 'VGA' ,
54
- width : 640 ,
55
- height : 480 ,
56
- ratio : '4:3' ,
57
- } , {
58
- label : 'CIF' ,
59
- width : 352 ,
60
- height : 288 ,
61
- ratio : '4:3' ,
62
- } , {
63
- label : 'QVGA' ,
64
- width : 320 ,
65
- height : 240 ,
66
- ratio : '4:3' ,
67
- } , {
68
- label : 'QCIF' ,
69
- width : 176 ,
70
- height : 144 ,
71
- ratio : '4:3' ,
72
- } , {
73
- label : 'QQVGA' ,
74
- width : 160 ,
75
- height : 120 ,
76
- ratio : '4:3' ,
77
- } ] ;
78
-
79
- function getUserMedia ( constraints ) {
80
- if ( navigator . mediaDevices && navigator . mediaDevices . getUserMedia ) {
81
- return navigator . mediaDevices . getUserMedia ( constraints ) ;
82
- }
83
-
84
- // Some browsers partially implement mediaDevices. We can't just assign an object
85
- // with getUserMedia as it would overwrite existing properties.
86
- // Here, we will just add the getUserMedia property if it's missing.
87
-
88
- // First get hold of the legacy getUserMedia, if present
89
- const get = navigator . getUserMedia
90
- || navigator . webkitGetUserMedia
91
- || navigator . mozGetUserMedia
92
- || ( ( ) => {
93
- const error = new Error ( 'Permission unimplemented' ) ;
94
- error . code = 0 ;
95
- error . name = 'NotAllowedError' ;
96
- throw error ;
97
- } ) ;
98
-
99
- return new Promise ( ( resolve , reject ) => {
100
- get . call ( navigator , constraints , resolve , reject ) ;
101
- } ) ;
102
- }
14
+ const Video = React . forwardRef (
15
+ ( props , ref ) => createElement (
16
+ 'video' ,
17
+ { ...props , ref } ,
18
+ ) ,
19
+ ) ;
103
20
104
21
const Camera = ( { children, containerStyle, onCameraReady, title } , ref ) => {
22
+ const videoRef = useRef ( ) ;
23
+ const expoCameraRef = useRef ( ) ;
105
24
const windowDimensions = useWindowDimensions ( ) ;
106
- const videoEl = useRef ( ) ;
107
- const canvasEl = useRef ( ) ;
108
- const [ candidate , setCandidate ] = useState ( ) ;
109
- const [ loading , setLoading ] = useState ( false ) ;
110
-
111
- const { width : videoWidth , height : videoHeight } = useMemo ( ( ) => {
112
- if ( ! candidate ) { return { width : 0 , height : 0 } ; }
113
- return getSize ( candidate . test . ratio , windowDimensions , 'number' ) ;
114
- } , [ candidate , windowDimensions ] ) ;
115
-
116
- const gum = useCallback ( async ( test , device ) => {
117
- if ( window . stream ) { window . stream . getTracks ( ) . forEach ( ( track ) => track . stop ( ) ) ; }
118
-
119
- const OS = utils . getOS ( ) ;
120
- const facingMode = [ 'iOS' , 'Android' ] . includes ( OS ) ? { exact : 'environment' } : 'environment' ;
121
25
122
- const constraints = {
123
- audio : false ,
124
- video : {
125
- facingMode,
126
- deviceId : device . deviceId ? { exact : device . deviceId } : undefined ,
127
- width : { exact : test . width } ,
128
- height : { exact : test . height } ,
129
- } ,
130
- } ;
131
-
132
- let result ;
133
- try {
134
- result = await getUserMedia ( constraints ) ;
135
- } catch ( error ) { result = { error } ; }
136
-
137
- return [ test , result , constraints , device ] ;
138
- } , [ ] ) ;
26
+ const [ camera , setCamera ] = useState ( {
27
+ stream : null ,
28
+ width : 0 ,
29
+ height : 0 ,
30
+ ratio : '4:3' ,
31
+ pictureSize : '' ,
32
+ } ) ;
139
33
140
- const findBestCandidate = useCallback ( async ( devices ) => {
141
- const testResults = devices
142
- . map ( ( device ) => tests . map ( async ( test ) => gum ( test , device ) ) )
143
- . flat ( ) ;
34
+ const [ loading , setLoading ] = useState ( false ) ;
144
35
145
- const [ test , stream , constraint ] = await testResults
146
- . reduce ( async ( resultA , resultB ) => {
147
- const [ testA , streamA ] = await resultA ;
148
- const [ testB , streamB ] = await resultB ;
36
+ const videoSize = useMemo (
37
+ ( ) => getSize ( camera . ratio , windowDimensions , 'number' ) ,
38
+ [ camera , windowDimensions ] ,
39
+ ) ;
149
40
150
- if ( streamA . error ) { return resultB ; }
151
- if ( streamB . error ) { return resultA ; }
41
+ useImperativeHandle ( ref , ( ) => ( {
42
+ async takePicture ( ) {
43
+ if ( ! videoRef . current ) { throw new Error ( 'Camera is not ready!' ) ; }
44
+ const { width, height, stream } = camera ;
152
45
153
- return testA . width > testB . width ? resultA : resultB ;
154
- } ) ;
46
+ if ( ImageCapture && getOS ( ) !== 'ios' ) {
47
+ const track = stream . getVideoTracks ( ) [ 0 ] ;
48
+ const imageCapture = new ImageCapture ( track ) ;
155
49
156
- return {
157
- test,
158
- stream,
159
- constraint,
160
- ask : `${ test . width } x${ test . height } ` ,
161
- browserVer : `${ adapter . browserDetails . browser } ${ adapter . browserDetails . version } ` ,
162
- } ;
163
- } , [ gum ] ) ;
50
+ const blob = await imageCapture . takePhoto ( {
51
+ imageWidth : width ,
52
+ imageHeight : height ,
53
+ } ) ;
164
54
165
- const findDevices = useCallback ( async ( ) => {
166
- const constraints = { audio : false , video : { facingMode : 'environment' } } ;
167
- const mediaDevices = await navigator . mediaDevices . enumerateDevices ( ) ;
168
- window . stream = await getUserMedia ( constraints ) ;
55
+ const uri = URL . createObjectURL ( blob ) ;
169
56
170
- return mediaDevices . filter ( ( { kind } ) => kind === 'videoinput' ) ;
171
- } , [ ] ) ;
57
+ return { uri } ;
58
+ }
172
59
173
- useImperativeHandle (
174
- ref ,
175
- ( ) => ( {
176
- async takePicture ( ) {
177
- if ( ! videoEl . current || videoEl . current ?. readyState !== videoEl . current ?. HAVE_ENOUGH_DATA ) {
178
- throw new Error (
179
- 'ERR_CAMERA_NOT_READY' ,
180
- 'HTMLVideoElement does not have enough camera data to construct an image yet.' ,
181
- ) ;
182
- }
60
+ const canvas = document . createElement ( 'canvas' ) ;
61
+ canvas . width = width ;
62
+ canvas . height = height ;
183
63
184
- canvasEl . current
185
- . getContext ( '2d' )
186
- . drawImage ( videoEl . current , 0 , 0 , canvasEl . current . width , canvasEl . current . height ) ;
64
+ canvas . getContext ( '2d' )
65
+ . drawImage ( videoRef . current , 0 , 0 , width , height ) ;
187
66
188
- const imageType = utils . getOS ( ) === 'ios' ? 'image/png' : 'image/webp' ;
189
- const uri = canvasEl . current . toDataURL ( imageType ) ;
67
+ const imageType = utils . getOS ( ) === 'ios' ? 'image/png' : 'image/webp' ;
68
+ const uri = canvas . toDataURL ( imageType ) ;
190
69
191
- return { uri } ;
192
- } ,
193
- async resumePreview ( ) {
194
- if ( videoEl . current ) {
195
- videoEl . current . play ( ) ;
196
- }
197
- } ,
198
- async pausePreview ( ) {
199
- if ( videoEl . current ) {
200
- videoEl . current . pause ( ) ;
201
- }
202
- } ,
203
- } ) ,
204
- [ videoEl ] ,
205
- ) ;
70
+ return { uri } ;
71
+ } ,
72
+ async resumePreview ( ) {
73
+ if ( videoRef . current ) {
74
+ videoRef . current . play ( ) ;
75
+ }
76
+ } ,
77
+ async pausePreview ( ) {
78
+ if ( videoRef . current ) {
79
+ videoRef . current . pause ( ) ;
80
+ }
81
+ } ,
82
+ } ) ,
83
+ [ camera ] ) ;
206
84
207
85
useLayoutEffect ( ( ) => {
208
86
( async ( ) => {
209
- if ( videoEl && ! candidate && ! loading ) {
210
- const video = videoEl . current ;
211
-
87
+ if ( videoRef && ! camera . stream && ! loading ) {
212
88
setLoading ( true ) ;
213
89
214
- const canvas = canvasEl . current ;
215
90
const devices = await findDevices ( ) ;
216
91
const bestCandidate = await findBestCandidate ( devices ) ;
217
92
218
- setCandidate ( bestCandidate ) ;
219
-
220
- if ( 'srcObject' in video ) {
221
- video . srcObject = bestCandidate . stream ;
222
- video . play ( ) ;
223
- } else {
224
- video . src = window . URL . createObjectURL ( bestCandidate . stream ) ;
225
- }
226
-
227
- canvas . width = bestCandidate . test . width ;
228
- canvas . height = bestCandidate . test . height ;
229
-
230
- video . onloadedmetadata = ( ) => { video . play ( ) ; } ;
93
+ setVideoSource ( videoRef . current , bestCandidate . stream ) ;
231
94
95
+ setCamera ( bestCandidate ) ;
232
96
setLoading ( false ) ;
97
+
233
98
onCameraReady ( ) ;
234
99
}
235
100
} ) ( ) ;
236
- } , [ candidate , findBestCandidate , findDevices , loading , onCameraReady , videoEl ] ) ;
101
+ } , [ camera , loading , onCameraReady , videoRef ] ) ;
237
102
238
103
return (
239
104
< View
@@ -245,13 +110,11 @@ const Camera = ({ children, containerStyle, onCameraReady, title }, ref) => {
245
110
< Video
246
111
autoPlay
247
112
playsInline
248
- ref = { videoEl }
249
- width = { videoWidth }
250
- height = { videoHeight }
113
+ ref = { videoRef }
114
+ { ...videoSize }
251
115
/>
252
- < canvas ref = { canvasEl } />
253
116
{ children }
254
- { ( title !== '' && candidate ) && < Text style = { styles . title } > { title } </ Text > }
117
+ < Text style = { styles . title } > { title } </ Text >
255
118
</ View >
256
119
) ;
257
120
} ;
0 commit comments