1'use strict'; 2 3import { Audio } from 'expo-av'; 4import { Asset } from 'expo-asset'; 5import { Platform } from 'react-native'; 6 7import { retryForStatus, waitFor } from './helpers'; 8 9export const name = 'Audio'; 10const mainTestingSource = require('../assets/LLizard.mp3'); 11const soundUri = 'http://www.noiseaddicts.com/samples_1w72b820/280.mp3'; 12const hlsStreamUri = 'http://qthttp.apple.com.edgesuite.net/1010qwoeiuryfg/sl.m3u8'; 13const hlsStreamUriWithRedirect = 'http://bit.ly/1iy90bn'; 14const redirectingSoundUri = 'http://bit.ly/2qBMx80'; 15const authenticatedStaticFilesBackend = 'https://authenticated-static-files-hagckpsbra.now.sh'; 16 17export function test(t) { 18 t.describe('Audio class', () => { 19 t.describe('Audio.setAudioModeAsync', () => { 20 // These tests should work according to the documentation, 21 // but the implementation doesn't return anything from the Promise. 22 23 // t.it('sets one set of the options', async () => { 24 // const mode = { 25 // playsInSilentModeIOS: true, 26 // allowsRecordingIOS: true, 27 // interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DUCK_OTHERS, 28 // shouldDuckAndroid: true, 29 // interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DUCK_OTHERS, 30 // playThroughEarpieceAndroid: false, 31 // }; 32 // try { 33 // const receivedMode = await Audio.setAudioModeAsync(mode); 34 // t.expect(receivedMode).toBeDefined(); 35 // receivedMode && t.expect(receivedMode).toEqual(t.jasmine.objectContaining(mode)); 36 // } catch (error) { 37 // t.fail(error); 38 // } 39 // }); 40 41 // t.it('sets another set of the options', async () => { 42 // const mode = { 43 // playsInSilentModeIOS: false, 44 // allowsRecordingIOS: false, 45 // interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX, 46 // shouldDuckAndroid: false, 47 // interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX, 48 // playThroughEarpieceAndroid: false, 49 // }; 50 // try { 51 // const receivedMode = await Audio.setAudioModeAsync(mode); 52 // t.expect(receivedMode).toBeDefined(); 53 // receivedMode && t.expect(receivedMode).toEqual(t.jasmine.objectContaining(mode)); 54 // } catch (error) { 55 // t.fail(error); 56 // } 57 // }); 58 59 if (Platform.OS === 'ios') { 60 t.it('rejects an invalid promise', async () => { 61 const mode = { 62 playsInSilentModeIOS: false, 63 allowsRecordingIOS: true, 64 interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX, 65 shouldDuckAndroid: false, 66 interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX, 67 playThroughEarpieceAndroid: false, 68 staysActiveInBackground: false, 69 }; 70 let error = null; 71 try { 72 await Audio.setAudioModeAsync(mode); 73 } catch (err) { 74 error = err; 75 } 76 t.expect(error).not.toBeNull(); 77 error && t.expect(error.toString()).toMatch('Impossible audio mode'); 78 }); 79 } 80 }); 81 }); 82 83 t.describe('Audio instances', () => { 84 let soundObject = null; 85 86 t.beforeAll(async () => { 87 await Audio.setIsEnabledAsync(true); 88 }); 89 90 t.beforeEach(() => { 91 soundObject = new Audio.Sound(); 92 }); 93 94 t.afterEach(async () => { 95 await soundObject.unloadAsync(); 96 soundObject = null; 97 }); 98 99 t.describe('Audio.loadAsync', () => { 100 t.it('loads the file with `require`', async () => { 101 await soundObject.loadAsync(require('../assets/LLizard.mp3')); 102 await retryForStatus(soundObject, { isLoaded: true }); 103 }); 104 105 t.it('loads the file from `Asset`', async () => { 106 await soundObject.loadAsync(Asset.fromModule(require('../assets/LLizard.mp3'))); 107 await retryForStatus(soundObject, { isLoaded: true }); 108 }); 109 110 t.it('loads the file from the Internet', async () => { 111 await soundObject.loadAsync({ uri: soundUri }); 112 await retryForStatus(soundObject, { isLoaded: true }); 113 }); 114 115 t.describe('cookie session', () => { 116 t.afterEach(async () => { 117 try { 118 await fetch(`${authenticatedStaticFilesBackend}/sign_out`, { 119 method: 'DELETE', 120 credentials: true, 121 }); 122 } catch (error) { 123 console.warn(`Could not sign out of cookie session test backend, error: ${error}.`); 124 } 125 }); 126 127 t.it( 128 'is shared with fetch session', 129 async () => { 130 let error = null; 131 try { 132 await soundObject.loadAsync({ 133 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 134 }); 135 } catch (err) { 136 error = err; 137 } 138 t.expect(error).toBeDefined(); 139 if (Platform.OS === 'android') { 140 t.expect(error.toString()).toMatch('Response code: 401'); 141 } else { 142 t.expect(error.toString()).toMatch('error code -1013'); 143 } 144 const signInResponse = await (await fetch( 145 `${authenticatedStaticFilesBackend}/sign_in`, 146 { 147 method: 'POST', 148 credentials: true, 149 } 150 )).text(); 151 t.expect(signInResponse).toMatch('Signed in successfully!'); 152 error = null; 153 try { 154 await soundObject.loadAsync({ 155 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 156 }); 157 } catch (err) { 158 error = err; 159 } 160 t.expect(error).toBeNull(); 161 }, 162 30000 163 ); 164 }); 165 166 t.it( 167 'supports adding custom headers to media request', 168 async () => { 169 let error = null; 170 try { 171 await soundObject.loadAsync({ 172 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 173 }); 174 } catch (err) { 175 error = err; 176 } 177 if (!error) { 178 throw new Error('Backend unexpectedly allowed unauthenticated request.'); 179 } 180 error = null; 181 try { 182 await soundObject.loadAsync({ 183 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 184 headers: { 185 authorization: 'mellon', 186 }, 187 }); 188 } catch (err) { 189 error = err; 190 } 191 t.expect(error).toBeNull(); 192 }, 193 30000 194 ); 195 196 if (Platform.OS === 'android') { 197 t.it( 198 'supports adding custom headers to media request (MediaPlayer implementation)', 199 async () => { 200 let error = null; 201 try { 202 await soundObject.loadAsync({ 203 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 204 androidImplementation: 'MediaPlayer', 205 }); 206 } catch (err) { 207 error = err; 208 } 209 if (!error) { 210 throw new Error('Backend unexpectedly allowed unauthenticated request.'); 211 } 212 error = null; 213 try { 214 await soundObject.loadAsync({ 215 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 216 androidImplementation: 'MediaPlayer', 217 headers: { 218 authorization: 'mellon', 219 }, 220 }); 221 } catch (err) { 222 error = err; 223 } 224 t.expect(error).toBeNull(); 225 } 226 ); 227 } 228 229 t.it('redirects from HTTPS URL to HTTPS URL (301)', async () => { 230 let error = null; 231 try { 232 await soundObject.loadAsync({ 233 uri: 234 'https://player.vimeo.com/external/181375362.sd.mp4?s=cf573e9cf7d747f4eaf7e57eeec88e9b22e3933f&profile_id=165', 235 }); 236 } catch (err) { 237 error = err; 238 } 239 t.expect(error).toBeNull(); 240 }); 241 242 if (Platform.OS === 'android') { 243 t.it( 244 'rejects the file from the Internet that redirects to non-standard content', 245 async () => { 246 let hasBeenRejected = false; 247 try { 248 await soundObject.loadAsync({ 249 uri: hlsStreamUriWithRedirect, 250 }); 251 await retryForStatus(soundObject, { isLoaded: true }); 252 } catch (error) { 253 hasBeenRejected = true; 254 } 255 t.expect(hasBeenRejected).toBe(true); 256 } 257 ); 258 t.it( 259 'loads the file from the Internet that redirects to non-standard content when overrideFileExtensionAndroid is provided', 260 async () => { 261 let hasBeenRejected = false; 262 try { 263 await soundObject.loadAsync({ 264 uri: hlsStreamUriWithRedirect, 265 overrideFileExtensionAndroid: 'm3u8', 266 }); 267 await retryForStatus(soundObject, { isLoaded: true }); 268 } catch (error) { 269 hasBeenRejected = true; 270 } 271 t.expect(hasBeenRejected).toBe(false); 272 } 273 ); 274 } else { 275 t.it( 276 'loads the file from the Internet that redirects to non-standard content', 277 async () => { 278 let hasBeenRejected = false; 279 try { 280 await soundObject.loadAsync({ 281 uri: hlsStreamUriWithRedirect, 282 }); 283 await retryForStatus(soundObject, { isLoaded: true }); 284 } catch (error) { 285 hasBeenRejected = true; 286 } 287 t.expect(hasBeenRejected).toBe(false); 288 } 289 ); 290 } 291 292 t.it('loads HLS stream', async () => { 293 await soundObject.loadAsync({ 294 uri: hlsStreamUri, 295 }); 296 await retryForStatus(soundObject, { isLoaded: true }); 297 }); 298 299 t.it('loads the file from the Internet (with redirecting URL)', async () => { 300 await soundObject.loadAsync({ 301 uri: redirectingSoundUri, 302 }); 303 await retryForStatus(soundObject, { isLoaded: true }); 304 }); 305 306 t.it('rejects if a file is already loaded', async () => { 307 await soundObject.loadAsync({ uri: soundUri }); 308 await retryForStatus(soundObject, { isLoaded: true }); 309 let hasBeenRejected = false; 310 try { 311 await soundObject.loadAsync(mainTestingSource); 312 } catch (error) { 313 hasBeenRejected = true; 314 error && t.expect(error.toString()).toMatch('already loaded'); 315 } 316 t.expect(hasBeenRejected).toBe(true); 317 }); 318 }); 319 320 t.describe('Audio.loadAsync(require, initialStatus)', () => { 321 t.it('sets an initial status', async () => { 322 const options = { 323 shouldPlay: true, 324 isLooping: true, 325 isMuted: false, 326 volume: 0.5, 327 rate: 1.5, 328 }; 329 await soundObject.loadAsync(mainTestingSource, options); 330 await retryForStatus(soundObject, options); 331 }); 332 }); 333 334 t.describe('Audio.setStatusAsync', () => { 335 t.it('sets a status', async () => { 336 const options = { 337 shouldPlay: true, 338 isLooping: true, 339 isMuted: false, 340 volume: 0.5, 341 rate: 1.5, 342 }; 343 await soundObject.loadAsync(mainTestingSource, options); 344 await retryForStatus(soundObject, options); 345 }); 346 }); 347 348 t.describe('Audio.unloadAsync(require, initialStatus)', () => { 349 t.it('unloads the object when it is loaded', async () => { 350 await soundObject.loadAsync(mainTestingSource); 351 await retryForStatus(soundObject, { isLoaded: true }); 352 await soundObject.unloadAsync(); 353 await retryForStatus(soundObject, { isLoaded: false }); 354 }); 355 356 t.it("rejects if the object isn't loaded", async () => { 357 let hasBeenRejected = false; 358 try { 359 await soundObject.unloadAsync(); 360 } catch (error) { 361 hasBeenRejected = true; 362 } 363 t.expect(hasBeenRejected).toBe(false); 364 }); 365 }); 366 367 /*t.describe('Audio.setOnPlaybackStatusUpdate', () => { 368 t.it('sets callbacks that gets called when playing and stopping', async () => { 369 const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); 370 soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); 371 await soundObject.loadAsync(mainTestingSource); 372 await retryForStatus(soundObject, { isLoaded: true }); 373 await soundObject.playAsync(); 374 await retryForStatus(soundObject, { isPlaying: true }); 375 await soundObject.stopAsync(); 376 await retryForStatus(soundObject, { isPlaying: false }); 377 t.expect(onPlaybackStatusUpdate).toHaveBeenCalledWith({ isLoaded: false }); 378 t 379 .expect(onPlaybackStatusUpdate) 380 .toHaveBeenCalledWith(t.jasmine.objectContaining({ isLoaded: true })); 381 t 382 .expect(onPlaybackStatusUpdate) 383 .toHaveBeenCalledWith(t.jasmine.objectContaining({ isPlaying: true })); 384 t 385 .expect(onPlaybackStatusUpdate) 386 .toHaveBeenCalledWith(t.jasmine.objectContaining({ isPlaying: false })); 387 }); 388 389 t.it( 390 'sets callbacks that gets called with didJustFinish when playback finishes', 391 async () => { 392 const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); 393 soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); 394 await soundObject.loadAsync(mainTestingSource); 395 await retryForStatus(soundObject, { isLoaded: true }); 396 await retryForStatus(soundObject, { isBuffering: false }); 397 const status = await soundObject.getStatusAsync(); 398 await soundObject.setStatusAsync({ 399 positionMillis: status.playableDurationMillis - 300, 400 shouldPlay: true, 401 }); 402 await new Promise(resolve => { 403 setTimeout(() => { 404 t 405 .expect(onPlaybackStatusUpdate) 406 .toHaveBeenCalledWith(t.jasmine.objectContaining({ didJustFinish: true })); 407 resolve(); 408 }, 1000); 409 }); 410 } 411 ); 412 });*/ 413 414 t.describe('Audio.playAsync', () => { 415 t.it('plays the sound', async () => { 416 await soundObject.loadAsync(mainTestingSource); 417 await soundObject.playAsync(); 418 await retryForStatus(soundObject, { isPlaying: true }); 419 }); 420 }); 421 422 t.describe('Audio.replayAsync', () => { 423 t.it('replays the sound', async () => { 424 await soundObject.loadAsync(mainTestingSource); 425 await retryForStatus(soundObject, { isLoaded: true }); 426 await soundObject.playAsync(); 427 await retryForStatus(soundObject, { isPlaying: true }); 428 await waitFor(500); 429 const statusBefore = await soundObject.getStatusAsync(); 430 soundObject.replayAsync(); 431 await retryForStatus(soundObject, { isPlaying: true }); 432 const statusAfter = await soundObject.getStatusAsync(); 433 t.expect(statusAfter.positionMillis).toBeLessThan(statusBefore.positionMillis); 434 }); 435 436 /*t.it('calls the onPlaybackStatusUpdate with hasJustBeenInterrupted = true', async () => { 437 const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); 438 await soundObject.loadAsync(mainTestingSource); 439 soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); 440 await retryForStatus(soundObject, { isLoaded: true }); 441 await soundObject.playAsync(); 442 await retryForStatus(soundObject, { isPlaying: true }); 443 await soundObject.replayAsync(); 444 t 445 .expect(onPlaybackStatusUpdate) 446 .toHaveBeenCalledWith(t.jasmine.objectContaining({ hasJustBeenInterrupted: true })); 447 });*/ 448 }); 449 450 t.describe('Audio.pauseAsync', () => { 451 t.it('pauses the sound', async () => { 452 await soundObject.loadAsync(mainTestingSource); 453 await soundObject.playAsync(); 454 await retryForStatus(soundObject, { isPlaying: true }); 455 await soundObject.pauseAsync(); 456 await retryForStatus(soundObject, { isPlaying: false }); 457 await soundObject.playAsync(); 458 await retryForStatus(soundObject, { isPlaying: true }); 459 }); 460 }); 461 462 t.describe('Audio.stopAsync', () => { 463 t.it('stops the sound', async () => { 464 await soundObject.loadAsync(mainTestingSource, { shouldPlay: true }); 465 await retryForStatus(soundObject, { isPlaying: true }); 466 await soundObject.stopAsync(); 467 await retryForStatus(soundObject, { isPlaying: false }); 468 }); 469 }); 470 471 t.describe('Audio.setPositionAsync', () => { 472 t.it('sets the position', async () => { 473 await soundObject.loadAsync(mainTestingSource); 474 await retryForStatus(soundObject, { positionMillis: 0 }); 475 await soundObject.setPositionAsync(1000); 476 await retryForStatus(soundObject, { positionMillis: 1000 }); 477 }); 478 }); 479 480 t.describe('Audio.setPositionAsync', () => { 481 t.it('sets the position', async () => { 482 await soundObject.loadAsync(mainTestingSource); 483 await retryForStatus(soundObject, { positionMillis: 0 }); 484 await soundObject.setPositionAsync(1000); 485 await retryForStatus(soundObject, { positionMillis: 1000 }); 486 }); 487 488 t.it('sets the position with tolerance', async () => { 489 await soundObject.loadAsync(mainTestingSource); 490 await retryForStatus(soundObject, { positionMillis: 0 }); 491 await soundObject.setPositionAsync(999, { 492 toleranceMillisBefore: 0, 493 toleranceMillisAfter: 0, 494 }); 495 await retryForStatus(soundObject, { positionMillis: 999 }); 496 }); 497 }); 498 499 t.describe('Audio.setVolumeAsync', () => { 500 t.beforeEach(async () => { 501 await soundObject.loadAsync(mainTestingSource, { volume: 1 }); 502 await retryForStatus(soundObject, { volume: 1 }); 503 }); 504 505 t.it('sets the volume', async () => { 506 await soundObject.setVolumeAsync(0.5); 507 await retryForStatus(soundObject, { volume: 0.5 }); 508 }); 509 510 const testVolumeFailure = (valueDescription, value) => 511 t.it(`rejects if volume value is ${valueDescription}`, async () => { 512 let hasBeenRejected = false; 513 try { 514 await soundObject.setVolumeAsync(value); 515 } catch (error) { 516 hasBeenRejected = true; 517 error && t.expect(error.toString()).toMatch(/value .+ between/); 518 } 519 t.expect(hasBeenRejected).toBe(true); 520 }); 521 522 testVolumeFailure('too big', 2); 523 testVolumeFailure('negative', -0.5); 524 }); 525 526 t.describe('Audio.setIsMutedAsync', () => { 527 t.it('sets whether the audio is muted', async () => { 528 await soundObject.loadAsync(mainTestingSource, { isMuted: true }); 529 await retryForStatus(soundObject, { isMuted: true }); 530 await soundObject.setIsMutedAsync(false); 531 await retryForStatus(soundObject, { isMuted: false }); 532 }); 533 }); 534 535 t.describe('Audio.setIsLoopingAsync', () => { 536 t.it('sets whether the audio is looped', async () => { 537 await soundObject.loadAsync(mainTestingSource, { isLooping: false }); 538 await retryForStatus(soundObject, { isLooping: false }); 539 await soundObject.setIsLoopingAsync(true); 540 await retryForStatus(soundObject, { isLooping: true }); 541 }); 542 }); 543 544 /*t.describe('Audio.setProgressUpdateIntervalAsync', () => { 545 t.it('sets update interval', async () => { 546 const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); 547 await soundObject.loadAsync(mainTestingSource, { shouldPlay: true }); 548 await retryForStatus(soundObject, { isPlaying: true }); 549 soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); 550 await soundObject.setProgressUpdateIntervalAsync(100); 551 await new Promise(resolve => { 552 setTimeout(() => { 553 t.expect(onPlaybackStatusUpdate.calls.count()).toBeGreaterThan(5); 554 resolve(); 555 }, 800); 556 }); 557 }); 558 });*/ 559 560 t.describe('Audio.setRateAsync', () => { 561 let rate = 0; 562 let shouldError = false; 563 let shouldCorrectPitch = false; 564 let pitchCorrectionQuality = Audio.PitchCorrectionQuality.Low; 565 566 t.beforeEach(async () => { 567 const rate = 0.9; 568 569 const status = await soundObject.loadAsync(mainTestingSource, { rate }); 570 t.expect(status.rate).toBeCloseTo(rate, 2); 571 }); 572 573 t.afterEach(async () => { 574 let hasBeenRejected = false; 575 576 try { 577 const status = await soundObject.setRateAsync(rate, shouldCorrectPitch, pitchCorrectionQuality); 578 t.expect(status.rate).toBeCloseTo(rate, 2); 579 t.expect(status.shouldCorrectPitch).toBe(shouldCorrectPitch); 580 t.expect(status.pitchCorrectionQuality).toBe(pitchCorrectionQuality); 581 } catch (error) { 582 hasBeenRejected = true; 583 } 584 585 t.expect(hasBeenRejected).toEqual(shouldError); 586 587 rate = 0; 588 shouldError = false; 589 shouldCorrectPitch = false; 590 }); 591 592 t.it('sets rate with shouldCorrectPitch = true', async () => { 593 rate = 1.5; 594 shouldCorrectPitch = true; 595 }); 596 597 t.it('sets rate with shouldCorrectPitch = false', async () => { 598 rate = 0.75; 599 shouldCorrectPitch = false; 600 }); 601 602 t.it('sets pitchCorrectionQuality to Low', async () => { 603 rate = 0.5; 604 shouldCorrectPitch = true; 605 pitchCorrectionQuality = Audio.PitchCorrectionQuality.Low; 606 }); 607 608 t.it('sets pitchCorrectionQuality to Medium', async () => { 609 pitchCorrectionQuality = Audio.PitchCorrectionQuality.Medium; 610 }); 611 612 t.it('sets pitchCorrectionQuality to High', async () => { 613 pitchCorrectionQuality = Audio.PitchCorrectionQuality.High; 614 }); 615 616 t.it('rejects too high rate', async () => { 617 rate = 40; 618 shouldError = true; 619 }); 620 621 t.it('rejects negative rate', async () => { 622 rate = -10; 623 shouldError = true; 624 }); 625 }); 626 }); 627} 628