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