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