1import { Audio, Video } from 'expo-av'; 2import { Camera } from 'expo-camera'; 3import React from 'react'; 4import { Platform } from 'react-native'; 5 6import * as TestUtils from '../TestUtils'; 7import { waitFor, mountAndWaitFor as originalMountAndWaitFor, retryForStatus } from './helpers'; 8 9export const name = 'Camera'; 10const style = { width: 200, height: 200 }; 11 12export async function test(t, { setPortalChild, cleanupPortal }) { 13 const shouldSkipTestsRequiringPermissions = await TestUtils.shouldSkipTestsRequiringPermissionsAsync(); 14 const describeWithPermissions = shouldSkipTestsRequiringPermissions ? t.xdescribe : t.describe; 15 16 describeWithPermissions('Camera', () => { 17 let instance = null; 18 let originalTimeout; 19 20 const refSetter = ref => { 21 instance = ref; 22 }; 23 24 const mountAndWaitFor = (child, propName = 'onCameraReady') => 25 new Promise(resolve => { 26 const response = originalMountAndWaitFor(child, propName, setPortalChild); 27 setTimeout(() => resolve(response), 1500); 28 }); 29 30 t.beforeAll(async () => { 31 await TestUtils.acceptPermissionsAndRunCommandAsync(() => { 32 return Camera.requestPermissionsAsync(); 33 }); 34 await TestUtils.acceptPermissionsAndRunCommandAsync(() => { 35 return Audio.requestPermissionsAsync(); 36 }); 37 38 originalTimeout = t.jasmine.DEFAULT_TIMEOUT_INTERVAL; 39 t.jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout * 3; 40 }); 41 42 t.afterAll(() => { 43 t.jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; 44 }); 45 46 t.beforeEach(async () => { 47 const { status } = await Camera.getPermissionsAsync(); 48 t.expect(status).toEqual('granted'); 49 }); 50 51 t.afterEach(async () => { 52 instance = null; 53 await cleanupPortal(); 54 }); 55 56 if (Platform.OS === 'android') { 57 t.describe('Camera.getSupportedRatiosAsync', () => { 58 t.it('returns an array of strings', async () => { 59 await mountAndWaitFor(<Camera style={style} ref={refSetter} />); 60 const ratios = await instance.getSupportedRatiosAsync(); 61 t.expect(ratios instanceof Array).toBe(true); 62 t.expect(ratios.length).toBeGreaterThan(0); 63 }); 64 }); 65 } 66 67 // NOTE(2020-06-03): These tests are very flaky on Android so we're disabling them for now 68 if (Platform.OS !== 'android') { 69 t.describe('Camera.takePictureAsync', () => { 70 t.it('returns a local URI', async () => { 71 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 72 const picture = await instance.takePictureAsync(); 73 t.expect(picture).toBeDefined(); 74 t.expect(picture.uri).toMatch(/^file:\/\//); 75 }); 76 77 t.it('returns `width` and `height` of the image', async () => { 78 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 79 const picture = await instance.takePictureAsync(); 80 t.expect(picture).toBeDefined(); 81 t.expect(picture.width).toBeDefined(); 82 t.expect(picture.height).toBeDefined(); 83 }); 84 85 t.it('returns EXIF only if requested', async () => { 86 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 87 let picture = await instance.takePictureAsync({ exif: false }); 88 t.expect(picture).toBeDefined(); 89 t.expect(picture.exif).not.toBeDefined(); 90 91 picture = await instance.takePictureAsync({ exif: true }); 92 t.expect(picture).toBeDefined(); 93 t.expect(picture.exif).toBeDefined(); 94 }); 95 96 t.it( 97 `returns Base64 only if requested, and not contains newline and 98 special characters (\n or \r)`, 99 async () => { 100 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 101 let picture = await instance.takePictureAsync({ base64: false }); 102 t.expect(picture).toBeDefined(); 103 t.expect(picture.base64).not.toBeDefined(); 104 105 picture = await instance.takePictureAsync({ base64: true }); 106 t.expect(picture).toBeDefined(); 107 t.expect(picture.base64).toBeDefined(); 108 t.expect(picture.base64).not.toContain('\n'); 109 t.expect(picture.base64).not.toContain('\r'); 110 } 111 ); 112 113 t.it('returns proper `exif.Flash % 2 = 0` if the flash is off', async () => { 114 await mountAndWaitFor( 115 <Camera ref={refSetter} flashMode={Camera.Constants.FlashMode.off} style={style} /> 116 ); 117 const picture = await instance.takePictureAsync({ exif: true }); 118 t.expect(picture).toBeDefined(); 119 t.expect(picture.exif).toBeDefined(); 120 t.expect(picture.exif.Flash % 2 === 0).toBe(true); 121 }); 122 123 if (Platform.OS === 'ios') { 124 // https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/flash.html 125 // Android returns invalid values! (I've tested the code on an Android tablet 126 // that has no flash and it returns Flash = 0, meaning that the flash did not fire, 127 // but is present.) 128 129 t.it('returns proper `exif.Flash % 2 = 1` if the flash is on', async () => { 130 await mountAndWaitFor( 131 <Camera ref={refSetter} flashMode={Camera.Constants.FlashMode.on} style={style} /> 132 ); 133 const picture = await instance.takePictureAsync({ exif: true }); 134 t.expect(picture).toBeDefined(); 135 t.expect(picture.exif).toBeDefined(); 136 t.expect(picture.exif.Flash % 2 === 1).toBe(true); 137 }); 138 } 139 140 // https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/whitebalance.html 141 142 t.it('returns `exif.WhiteBalance = 1` if white balance is manually set', async () => { 143 await mountAndWaitFor( 144 <Camera 145 style={style} 146 ref={refSetter} 147 whiteBalance={Camera.Constants.WhiteBalance.incandescent} 148 /> 149 ); 150 const picture = await instance.takePictureAsync({ exif: true }); 151 t.expect(picture).toBeDefined(); 152 t.expect(picture.exif).toBeDefined(); 153 t.expect(picture.exif.WhiteBalance).toEqual(1); 154 }); 155 156 t.it('returns `exif.WhiteBalance = 0` if white balance is set to auto', async () => { 157 await mountAndWaitFor( 158 <Camera 159 style={style} 160 ref={refSetter} 161 whiteBalance={Camera.Constants.WhiteBalance.auto} 162 /> 163 ); 164 const picture = await instance.takePictureAsync({ exif: true }); 165 t.expect(picture).toBeDefined(); 166 t.expect(picture.exif).toBeDefined(); 167 t.expect(picture.exif.WhiteBalance).toEqual(0); 168 }); 169 170 if (Platform.OS === 'ios') { 171 t.it('returns `exif.LensModel ~= back` if camera type is set to back', async () => { 172 await mountAndWaitFor( 173 <Camera style={style} ref={refSetter} type={Camera.Constants.Type.back} /> 174 ); 175 const picture = await instance.takePictureAsync({ exif: true }); 176 t.expect(picture).toBeDefined(); 177 t.expect(picture.exif).toBeDefined(); 178 t.expect(picture.exif.LensModel).toMatch('back'); 179 await cleanupPortal(); 180 }); 181 182 t.it('returns `exif.LensModel ~= front` if camera type is set to front', async () => { 183 await mountAndWaitFor( 184 <Camera style={style} ref={refSetter} type={Camera.Constants.Type.front} /> 185 ); 186 const picture = await instance.takePictureAsync({ exif: true }); 187 t.expect(picture).toBeDefined(); 188 t.expect(picture.exif).toBeDefined(); 189 t.expect(picture.exif.LensModel).toMatch('front'); 190 await cleanupPortal(); 191 }); 192 193 t.it('returns `exif.DigitalZoom ~= false` if zoom is not set', async () => { 194 await mountAndWaitFor(<Camera style={style} ref={refSetter} />); 195 const picture = await instance.takePictureAsync({ exif: true }); 196 t.expect(picture).toBeDefined(); 197 t.expect(picture.exif).toBeDefined(); 198 t.expect(picture.exif.DigitalZoomRatio).toBeFalsy(); 199 await cleanupPortal(); 200 }); 201 202 t.it('returns `exif.DigitalZoom ~= false` if zoom is set to 0', async () => { 203 await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={0} />); 204 const picture = await instance.takePictureAsync({ exif: true }); 205 t.expect(picture).toBeDefined(); 206 t.expect(picture.exif).toBeDefined(); 207 t.expect(picture.exif.DigitalZoomRatio).toBeFalsy(); 208 await cleanupPortal(); 209 }); 210 211 let smallerRatio = null; 212 213 t.it('returns `exif.DigitalZoom > 0` if zoom is set', async () => { 214 await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={0.5} />); 215 const picture = await instance.takePictureAsync({ exif: true }); 216 t.expect(picture).toBeDefined(); 217 t.expect(picture.exif).toBeDefined(); 218 t.expect(picture.exif.DigitalZoomRatio).toBeGreaterThan(0); 219 smallerRatio = picture.exif.DigitalZoomRatio; 220 await cleanupPortal(); 221 }); 222 223 t.it( 224 'returns `exif.DigitalZoom`s monotonically increasing with the zoom value', 225 async () => { 226 await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={1} />); 227 const picture = await instance.takePictureAsync({ exif: true }); 228 t.expect(picture).toBeDefined(); 229 t.expect(picture.exif).toBeDefined(); 230 t.expect(picture.exif.DigitalZoomRatio).toBeGreaterThan(smallerRatio); 231 await cleanupPortal(); 232 } 233 ); 234 } 235 }); 236 } 237 238 t.describe('Camera.recordAsync', () => { 239 t.beforeEach(async () => { 240 if (Platform.OS === 'ios') { 241 await waitFor(500); 242 } 243 }); 244 245 t.it('returns a local URI', async () => { 246 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 247 const recordingPromise = instance.recordAsync(); 248 await waitFor(2500); 249 instance.stopRecording(); 250 const response = await recordingPromise; 251 t.expect(response).toBeDefined(); 252 t.expect(response.uri).toMatch(/^file:\/\//); 253 }); 254 255 if (Platform.OS === 'ios') { 256 t.it('throws for an unavailable codec', async () => { 257 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 258 259 await instance 260 .recordAsync({ 261 codec: '123', 262 }) 263 .catch(error => { 264 t.expect(error.message).toMatch(/(?=.*codec)(?=.*is not supported)/i); 265 }); 266 }); 267 268 t.it('returns available codecs', async () => { 269 const codecs = await Camera.getAvailableVideoCodecsAsync(); 270 t.expect(codecs).toBeDefined(); 271 t.expect(codecs.length).toBeGreaterThan(0); 272 }); 273 } 274 275 let recordedFileUri = null; 276 277 t.it('stops the recording after maxDuration', async () => { 278 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 279 const response = await instance.recordAsync({ maxDuration: 2 }); 280 recordedFileUri = response.uri; 281 }); 282 283 t.it('the video has a duration near maxDuration', async () => { 284 await mountAndWaitFor( 285 <Video style={style} source={{ uri: recordedFileUri }} ref={refSetter} />, 286 'onLoad' 287 ); 288 await retryForStatus(instance, { isBuffering: false }); 289 const video = await instance.getStatusAsync(); 290 t.expect(video.durationMillis).toBeLessThan(2250); 291 t.expect(video.durationMillis).toBeGreaterThan(1750); 292 }); 293 294 // Test for the fix to: https://github.com/expo/expo/issues/1976 295 const testFrontCameraRecording = async camera => { 296 await mountAndWaitFor(camera); 297 const response = await instance.recordAsync({ maxDuration: 2 }); 298 299 await mountAndWaitFor( 300 <Video style={style} source={{ uri: response.uri }} ref={refSetter} />, 301 'onLoad' 302 ); 303 await retryForStatus(instance, { isBuffering: false }); 304 const video = await instance.getStatusAsync(); 305 306 t.expect(video.durationMillis).toBeLessThan(2250); 307 t.expect(video.durationMillis).toBeGreaterThan(1750); 308 }; 309 310 t.it('records using the front camera', async () => { 311 await testFrontCameraRecording( 312 <Camera 313 ref={refSetter} 314 style={style} 315 type={Camera.Constants.Type.front} 316 useCamera2Api={false} 317 /> 318 ); 319 }); 320 321 if (Platform.OS === 'android') { 322 t.it('records using the front camera and Camera2 API', async () => { 323 await testFrontCameraRecording( 324 <Camera 325 ref={refSetter} 326 style={style} 327 type={Camera.Constants.Type.front} 328 useCamera2Api 329 /> 330 ); 331 }); 332 } 333 334 t.it('stops the recording after maxFileSize', async () => { 335 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 336 await instance.recordAsync({ maxFileSize: 256 * 1024 }); // 256 KiB 337 }); 338 339 t.describe('can record consecutive clips', () => { 340 let defaultTimeoutInterval = null; 341 t.beforeAll(() => { 342 defaultTimeoutInterval = t.jasmine.DEFAULT_TIMEOUT_INTERVAL; 343 t.jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeoutInterval * 2; 344 }); 345 346 t.afterAll(() => { 347 t.jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeoutInterval; 348 }); 349 350 t.it('started/stopped manually', async () => { 351 await mountAndWaitFor(<Camera style={style} ref={refSetter} />); 352 353 const recordFor = duration => 354 new Promise(async (resolve, reject) => { 355 const recordingPromise = instance.recordAsync(); 356 await waitFor(duration); 357 instance.stopRecording(); 358 try { 359 const recordedVideo = await recordingPromise; 360 t.expect(recordedVideo).toBeDefined(); 361 t.expect(recordedVideo.uri).toBeDefined(); 362 resolve(); 363 } catch (error) { 364 reject(error); 365 } 366 }); 367 368 await recordFor(1000); 369 await waitFor(1000); 370 await recordFor(1000); 371 }); 372 373 t.it('started/stopped automatically', async () => { 374 await mountAndWaitFor(<Camera style={style} ref={refSetter} />); 375 376 const recordFor = duration => 377 new Promise(async (resolve, reject) => { 378 try { 379 const response = await instance.recordAsync({ maxDuration: duration / 1000 }); 380 resolve(response); 381 } catch (error) { 382 reject(error); 383 } 384 }); 385 386 await recordFor(1000); 387 await recordFor(1000); 388 }); 389 }); 390 }); 391 }); 392} 393