1import { Video } from 'expo-av'; 2import { Camera } from 'expo-camera'; 3import React from 'react'; 4import { Platform } from 'react-native'; 5 6import { waitFor, mountAndWaitFor as originalMountAndWaitFor, retryForStatus } from './helpers'; 7import * as TestUtils from '../TestUtils'; 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('adds additional EXIF only if requested', async () => { 112 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 113 const additionalExif = { 114 GPSLatitude: 30.82123, 115 GPSLongitude: 150.25582, 116 GPSAltitude: 80.808, 117 }; 118 let picture = await instance.takePictureAsync({ exif: false, additionalExif }); 119 t.expect(picture).toBeDefined(); 120 t.expect(picture.exif).not.toBeDefined(); 121 122 picture = await instance.takePictureAsync({ exif: true, additionalExif }); 123 t.expect(picture).toBeDefined(); 124 t.expect(picture.exif).toBeDefined(); 125 t.expect(picture.exif.GPSLatitude).toBe(additionalExif.GPSLatitude); 126 t.expect(picture.exif.GPSLongitude).toBe(additionalExif.GPSLongitude); 127 t.expect(picture.exif.GPSAltitude).toBe(additionalExif.GPSAltitude); 128 }); 129 130 t.it( 131 `returns Base64 only if requested, and not contains newline and 132 special characters (\n or \r)`, 133 async () => { 134 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 135 let picture = await instance.takePictureAsync({ base64: false }); 136 t.expect(picture).toBeDefined(); 137 t.expect(picture.base64).not.toBeDefined(); 138 139 picture = await instance.takePictureAsync({ base64: true }); 140 t.expect(picture).toBeDefined(); 141 t.expect(picture.base64).toBeDefined(); 142 t.expect(picture.base64).not.toContain('\n'); 143 t.expect(picture.base64).not.toContain('\r'); 144 } 145 ); 146 147 t.it('returns proper `exif.Flash % 2 = 0` if the flash is off', async () => { 148 await mountAndWaitFor( 149 <Camera ref={refSetter} flashMode={Camera.Constants.FlashMode.off} style={style} /> 150 ); 151 const picture = await instance.takePictureAsync({ exif: true }); 152 t.expect(picture).toBeDefined(); 153 t.expect(picture.exif).toBeDefined(); 154 t.expect(picture.exif.Flash % 2 === 0).toBe(true); 155 }); 156 157 if (Platform.OS === 'ios') { 158 // https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/flash.html 159 // Android returns invalid values! (I've tested the code on an Android tablet 160 // that has no flash and it returns Flash = 0, meaning that the flash did not fire, 161 // but is present.) 162 163 t.it('returns proper `exif.Flash % 2 = 1` if the flash is on', async () => { 164 await mountAndWaitFor( 165 <Camera ref={refSetter} flashMode={Camera.Constants.FlashMode.on} style={style} /> 166 ); 167 const picture = await instance.takePictureAsync({ exif: true }); 168 t.expect(picture).toBeDefined(); 169 t.expect(picture.exif).toBeDefined(); 170 t.expect(picture.exif.Flash % 2 === 1).toBe(true); 171 }); 172 } 173 174 // https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/whitebalance.html 175 176 t.it('returns `exif.WhiteBalance = 1` if white balance is manually set', async () => { 177 await mountAndWaitFor( 178 <Camera 179 style={style} 180 ref={refSetter} 181 whiteBalance={Camera.Constants.WhiteBalance.incandescent} 182 /> 183 ); 184 const picture = await instance.takePictureAsync({ exif: true }); 185 t.expect(picture).toBeDefined(); 186 t.expect(picture.exif).toBeDefined(); 187 t.expect(picture.exif.WhiteBalance).toEqual(1); 188 }); 189 190 t.it('returns `exif.WhiteBalance = 0` if white balance is set to auto', async () => { 191 await mountAndWaitFor( 192 <Camera 193 style={style} 194 ref={refSetter} 195 whiteBalance={Camera.Constants.WhiteBalance.auto} 196 /> 197 ); 198 const picture = await instance.takePictureAsync({ exif: true }); 199 t.expect(picture).toBeDefined(); 200 t.expect(picture.exif).toBeDefined(); 201 t.expect(picture.exif.WhiteBalance).toEqual(0); 202 }); 203 204 if (Platform.OS === 'ios') { 205 t.it('returns `exif.LensModel ~= back` if camera type is set to back', async () => { 206 await mountAndWaitFor( 207 <Camera style={style} ref={refSetter} type={Camera.Constants.Type.back} /> 208 ); 209 const picture = await instance.takePictureAsync({ exif: true }); 210 t.expect(picture).toBeDefined(); 211 t.expect(picture.exif).toBeDefined(); 212 t.expect(picture.exif.LensModel).toMatch('back'); 213 await cleanupPortal(); 214 }); 215 216 t.it('returns `exif.LensModel ~= front` if camera type is set to front', async () => { 217 await mountAndWaitFor( 218 <Camera style={style} ref={refSetter} type={Camera.Constants.Type.front} /> 219 ); 220 const picture = await instance.takePictureAsync({ exif: true }); 221 t.expect(picture).toBeDefined(); 222 t.expect(picture.exif).toBeDefined(); 223 t.expect(picture.exif.LensModel).toMatch('front'); 224 await cleanupPortal(); 225 }); 226 227 t.it('returns `exif.DigitalZoom ~= false` if zoom is not set', async () => { 228 await mountAndWaitFor(<Camera style={style} ref={refSetter} />); 229 const picture = await instance.takePictureAsync({ exif: true }); 230 t.expect(picture).toBeDefined(); 231 t.expect(picture.exif).toBeDefined(); 232 t.expect(picture.exif.DigitalZoomRatio).toBeFalsy(); 233 await cleanupPortal(); 234 }); 235 236 t.it('returns `exif.DigitalZoom ~= false` if zoom is set to 0', async () => { 237 await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={0} />); 238 const picture = await instance.takePictureAsync({ exif: true }); 239 t.expect(picture).toBeDefined(); 240 t.expect(picture.exif).toBeDefined(); 241 t.expect(picture.exif.DigitalZoomRatio).toBeFalsy(); 242 await cleanupPortal(); 243 }); 244 245 let smallerRatio = null; 246 247 t.it('returns `exif.DigitalZoom > 0` if zoom is set', async () => { 248 await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={0.5} />); 249 const picture = await instance.takePictureAsync({ exif: true }); 250 t.expect(picture).toBeDefined(); 251 t.expect(picture.exif).toBeDefined(); 252 t.expect(picture.exif.DigitalZoomRatio).toBeGreaterThan(0); 253 smallerRatio = picture.exif.DigitalZoomRatio; 254 await cleanupPortal(); 255 }); 256 257 t.it( 258 'returns `exif.DigitalZoom`s monotonically increasing with the zoom value', 259 async () => { 260 await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={1} />); 261 const picture = await instance.takePictureAsync({ exif: true }); 262 t.expect(picture).toBeDefined(); 263 t.expect(picture.exif).toBeDefined(); 264 t.expect(picture.exif.DigitalZoomRatio).toBeGreaterThan(smallerRatio); 265 await cleanupPortal(); 266 } 267 ); 268 } 269 }); 270 } 271 272 t.describe('Camera.recordAsync', () => { 273 t.beforeEach(async () => { 274 if (Platform.OS === 'ios') { 275 await waitFor(500); 276 } 277 }); 278 279 t.it('returns a local URI', async () => { 280 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 281 const recordingPromise = instance.recordAsync(); 282 await waitFor(2500); 283 instance.stopRecording(); 284 const response = await recordingPromise; 285 t.expect(response).toBeDefined(); 286 t.expect(response.uri).toMatch(/^file:\/\//); 287 }); 288 289 if (Platform.OS === 'ios') { 290 t.it('throws for an unavailable codec', async () => { 291 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 292 293 await instance 294 .recordAsync({ 295 codec: '123', 296 }) 297 .catch((error) => { 298 t.expect(error.message).toMatch(/(?=.*codec)(?=.*is not supported)/i); 299 }); 300 }); 301 302 t.it('returns available codecs', async () => { 303 const codecs = await Camera.getAvailableVideoCodecsAsync(); 304 t.expect(codecs).toBeDefined(); 305 t.expect(codecs.length).toBeGreaterThan(0); 306 }); 307 } 308 309 let recordedFileUri = null; 310 311 t.it('stops the recording after maxDuration', async () => { 312 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 313 const response = await instance.recordAsync({ maxDuration: 2 }); 314 recordedFileUri = response.uri; 315 }); 316 317 t.it('the video has a duration near maxDuration', async () => { 318 await mountAndWaitFor( 319 <Video style={style} source={{ uri: recordedFileUri }} ref={refSetter} />, 320 'onLoad' 321 ); 322 await retryForStatus(instance, { isBuffering: false }); 323 const video = await instance.getStatusAsync(); 324 t.expect(video.durationMillis).toBeLessThan(2250); 325 t.expect(video.durationMillis).toBeGreaterThan(1750); 326 }); 327 328 // Test for the fix to: https://github.com/expo/expo/issues/1976 329 const testFrontCameraRecording = async (camera) => { 330 await mountAndWaitFor(camera); 331 const response = await instance.recordAsync({ maxDuration: 2 }); 332 333 await mountAndWaitFor( 334 <Video style={style} source={{ uri: response.uri }} ref={refSetter} />, 335 'onLoad' 336 ); 337 await retryForStatus(instance, { isBuffering: false }); 338 const video = await instance.getStatusAsync(); 339 340 t.expect(video.durationMillis).toBeLessThan(2250); 341 t.expect(video.durationMillis).toBeGreaterThan(1750); 342 }; 343 344 t.it('records using the front camera', async () => { 345 await testFrontCameraRecording( 346 <Camera 347 ref={refSetter} 348 style={style} 349 type={Camera.Constants.Type.front} 350 useCamera2Api={false} 351 /> 352 ); 353 }); 354 355 if (Platform.OS === 'android') { 356 t.it('records using the front camera and Camera2 API', async () => { 357 await testFrontCameraRecording( 358 <Camera 359 ref={refSetter} 360 style={style} 361 type={Camera.Constants.Type.front} 362 useCamera2Api 363 /> 364 ); 365 }); 366 } 367 368 t.it('stops the recording after maxFileSize', async () => { 369 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 370 await instance.recordAsync({ maxFileSize: 256 * 1024 }); // 256 KiB 371 }); 372 373 t.describe('can record consecutive clips', () => { 374 let defaultTimeoutInterval = null; 375 t.beforeAll(() => { 376 defaultTimeoutInterval = t.jasmine.DEFAULT_TIMEOUT_INTERVAL; 377 t.jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeoutInterval * 2; 378 }); 379 380 t.afterAll(() => { 381 t.jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeoutInterval; 382 }); 383 384 t.it('started/stopped manually', async () => { 385 await mountAndWaitFor(<Camera style={style} ref={refSetter} />); 386 387 const recordFor = (duration) => 388 new Promise(async (resolve, reject) => { 389 const recordingPromise = instance.recordAsync(); 390 await waitFor(duration); 391 instance.stopRecording(); 392 try { 393 const recordedVideo = await recordingPromise; 394 t.expect(recordedVideo).toBeDefined(); 395 t.expect(recordedVideo.uri).toBeDefined(); 396 resolve(); 397 } catch (error) { 398 reject(error); 399 } 400 }); 401 402 await recordFor(1000); 403 await waitFor(1000); 404 await recordFor(1000); 405 }); 406 407 t.it('started/stopped automatically', async () => { 408 await mountAndWaitFor(<Camera style={style} ref={refSetter} />); 409 410 const recordFor = (duration) => 411 new Promise(async (resolve, reject) => { 412 try { 413 const response = await instance.recordAsync({ maxDuration: duration / 1000 }); 414 resolve(response); 415 } catch (error) { 416 reject(error); 417 } 418 }); 419 420 await recordFor(1000); 421 await recordFor(1000); 422 }); 423 }); 424 }); 425 }); 426} 427