1'use strict'; 2 3import { Asset } from 'expo-asset'; 4import { Audio, InterruptionModeAndroid, InterruptionModeIOS } from 'expo-av'; 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.vercel.app'; 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: InterruptionModeIOS.DoNotMix, 65 shouldDuckAndroid: false, 66 interruptionModeAndroid: InterruptionModeAndroid.DoNotMix, 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.message).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.message).toMatch('Response code: 401'); 141 } else { 142 t.expect(error.message).toMatch('error code -1013'); 143 } 144 const signInResponse = await ( 145 await fetch(`${authenticatedStaticFilesBackend}/sign_in`, { 146 method: 'POST', 147 credentials: true, 148 }) 149 ).text(); 150 t.expect(signInResponse).toMatch('Signed in successfully!'); 151 error = null; 152 try { 153 await soundObject.loadAsync({ 154 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 155 }); 156 } catch (err) { 157 error = err; 158 } 159 t.expect(error).toBeNull(); 160 }, 161 30000 162 ); 163 }); 164 165 t.it( 166 'supports adding custom headers to media request', 167 async () => { 168 let error = null; 169 try { 170 await soundObject.loadAsync({ 171 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 172 }); 173 } catch (err) { 174 error = err; 175 } 176 if (!error) { 177 throw new Error('Backend unexpectedly allowed unauthenticated request.'); 178 } 179 error = null; 180 try { 181 await soundObject.loadAsync({ 182 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 183 headers: { 184 authorization: 'mellon', 185 }, 186 }); 187 } catch (err) { 188 error = err; 189 } 190 t.expect(error).toBeNull(); 191 }, 192 30000 193 ); 194 195 if (Platform.OS === 'android') { 196 t.it( 197 'supports adding custom headers to media request (MediaPlayer implementation)', 198 async () => { 199 let error = null; 200 try { 201 await soundObject.loadAsync({ 202 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 203 androidImplementation: 'MediaPlayer', 204 }); 205 } catch (err) { 206 error = err; 207 } 208 if (!error) { 209 throw new Error('Backend unexpectedly allowed unauthenticated request.'); 210 } 211 error = null; 212 try { 213 await soundObject.loadAsync({ 214 uri: `${authenticatedStaticFilesBackend}/LLizard.mp3`, 215 androidImplementation: 'MediaPlayer', 216 headers: { 217 authorization: 'mellon', 218 }, 219 }); 220 } catch (err) { 221 error = err; 222 } 223 t.expect(error).toBeNull(); 224 } 225 ); 226 } 227 228 t.it('redirects from HTTPS URL to HTTPS URL (302)', async () => { 229 // Redirects link shortened URL to GitHub raw audio MP3 URL for LLizard.mp3 asset. 230 let error = null; 231 try { 232 await soundObject.loadAsync({ 233 uri: 'https://rb.gy/eodxez', 234 }); 235 } catch (err) { 236 error = err; 237 } 238 t.expect(error).toBeNull(); 239 }); 240 241 if (Platform.OS === 'android') { 242 t.it( 243 'rejects the file from the Internet that redirects to non-standard content', 244 async () => { 245 let hasBeenRejected = false; 246 try { 247 await soundObject.loadAsync({ 248 uri: hlsStreamUriWithRedirect, 249 }); 250 await retryForStatus(soundObject, { isLoaded: true }); 251 } catch { 252 hasBeenRejected = true; 253 } 254 t.expect(hasBeenRejected).toBe(true); 255 } 256 ); 257 t.it( 258 'loads the file from the Internet that redirects to non-standard content when overrideFileExtensionAndroid is provided', 259 async () => { 260 let hasBeenRejected = false; 261 try { 262 await soundObject.loadAsync({ 263 uri: hlsStreamUriWithRedirect, 264 overrideFileExtensionAndroid: 'm3u8', 265 }); 266 await retryForStatus(soundObject, { isLoaded: true }); 267 } catch { 268 hasBeenRejected = true; 269 } 270 t.expect(hasBeenRejected).toBe(false); 271 } 272 ); 273 } else { 274 t.it( 275 'loads the file from the Internet that redirects to non-standard content', 276 async () => { 277 let hasBeenRejected = false; 278 try { 279 await soundObject.loadAsync({ 280 uri: hlsStreamUriWithRedirect, 281 }); 282 await retryForStatus(soundObject, { isLoaded: true }); 283 } catch { 284 hasBeenRejected = true; 285 } 286 t.expect(hasBeenRejected).toBe(false); 287 } 288 ); 289 } 290 291 t.it('loads HLS stream', async () => { 292 await soundObject.loadAsync({ 293 uri: hlsStreamUri, 294 }); 295 await retryForStatus(soundObject, { isLoaded: true }); 296 }); 297 298 t.it('loads the file from the Internet (with redirecting URL)', async () => { 299 await soundObject.loadAsync({ 300 uri: redirectingSoundUri, 301 }); 302 await retryForStatus(soundObject, { isLoaded: true }); 303 }); 304 305 t.it('rejects if a file is already loaded', async () => { 306 await soundObject.loadAsync({ uri: soundUri }); 307 await retryForStatus(soundObject, { isLoaded: true }); 308 let hasBeenRejected = false; 309 try { 310 await soundObject.loadAsync(mainTestingSource); 311 } catch (error) { 312 hasBeenRejected = true; 313 error && t.expect(error.message).toMatch('already loaded'); 314 } 315 t.expect(hasBeenRejected).toBe(true); 316 }); 317 }); 318 319 t.describe('Audio.loadAsync(require, initialStatus)', () => { 320 t.it('sets an initial status', async () => { 321 const options = { 322 shouldPlay: true, 323 isLooping: true, 324 isMuted: false, 325 volume: 0.5, 326 audioPan: -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 audioPan: 0.5, 342 rate: 1.5, 343 }; 344 await soundObject.loadAsync(mainTestingSource, options); 345 await retryForStatus(soundObject, options); 346 }); 347 }); 348 349 t.describe('Audio.unloadAsync(require, initialStatus)', () => { 350 t.it('unloads the object when it is loaded', async () => { 351 await soundObject.loadAsync(mainTestingSource); 352 await retryForStatus(soundObject, { isLoaded: true }); 353 await soundObject.unloadAsync(); 354 await retryForStatus(soundObject, { isLoaded: false }); 355 }); 356 357 t.it("rejects if the object isn't loaded", async () => { 358 let hasBeenRejected = false; 359 try { 360 await soundObject.unloadAsync(); 361 } catch { 362 hasBeenRejected = true; 363 } 364 t.expect(hasBeenRejected).toBe(false); 365 }); 366 }); 367 368 /*t.describe('Audio.setOnPlaybackStatusUpdate', () => { 369 t.it('sets callbacks that gets called when playing and stopping', async () => { 370 const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); 371 soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); 372 await soundObject.loadAsync(mainTestingSource); 373 await retryForStatus(soundObject, { isLoaded: true }); 374 await soundObject.playAsync(); 375 await retryForStatus(soundObject, { isPlaying: true }); 376 await soundObject.stopAsync(); 377 await retryForStatus(soundObject, { isPlaying: false }); 378 t.expect(onPlaybackStatusUpdate).toHaveBeenCalledWith({ isLoaded: false }); 379 t 380 .expect(onPlaybackStatusUpdate) 381 .toHaveBeenCalledWith(t.jasmine.objectContaining({ isLoaded: true })); 382 t 383 .expect(onPlaybackStatusUpdate) 384 .toHaveBeenCalledWith(t.jasmine.objectContaining({ isPlaying: true })); 385 t 386 .expect(onPlaybackStatusUpdate) 387 .toHaveBeenCalledWith(t.jasmine.objectContaining({ isPlaying: false })); 388 }); 389 390 t.it( 391 'sets callbacks that gets called with didJustFinish when playback finishes', 392 async () => { 393 const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); 394 soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); 395 await soundObject.loadAsync(mainTestingSource); 396 await retryForStatus(soundObject, { isLoaded: true }); 397 await retryForStatus(soundObject, { isBuffering: false }); 398 const status = await soundObject.getStatusAsync(); 399 await soundObject.setStatusAsync({ 400 positionMillis: status.playableDurationMillis - 300, 401 shouldPlay: true, 402 }); 403 await new Promise(resolve => { 404 setTimeout(() => { 405 t 406 .expect(onPlaybackStatusUpdate) 407 .toHaveBeenCalledWith(t.jasmine.objectContaining({ didJustFinish: true })); 408 resolve(); 409 }, 1000); 410 }); 411 } 412 ); 413 });*/ 414 415 t.describe('Audio.playAsync', () => { 416 t.it('plays the sound', async () => { 417 await soundObject.loadAsync(mainTestingSource); 418 await soundObject.playAsync(); 419 await retryForStatus(soundObject, { isPlaying: true }); 420 }); 421 }); 422 423 t.describe('Audio.replayAsync', () => { 424 t.it('replays the sound', async () => { 425 await soundObject.loadAsync(mainTestingSource); 426 await retryForStatus(soundObject, { isLoaded: true }); 427 await soundObject.playAsync(); 428 await retryForStatus(soundObject, { isPlaying: true }); 429 await waitFor(500); 430 const statusBefore = await soundObject.getStatusAsync(); 431 soundObject.replayAsync(); 432 await retryForStatus(soundObject, { isPlaying: true }); 433 const statusAfter = await soundObject.getStatusAsync(); 434 t.expect(statusAfter.positionMillis).toBeLessThan(statusBefore.positionMillis); 435 }); 436 437 /*t.it('calls the onPlaybackStatusUpdate with hasJustBeenInterrupted = true', async () => { 438 const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); 439 await soundObject.loadAsync(mainTestingSource); 440 soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); 441 await retryForStatus(soundObject, { isLoaded: true }); 442 await soundObject.playAsync(); 443 await retryForStatus(soundObject, { isPlaying: true }); 444 await soundObject.replayAsync(); 445 t 446 .expect(onPlaybackStatusUpdate) 447 .toHaveBeenCalledWith(t.jasmine.objectContaining({ hasJustBeenInterrupted: true })); 448 });*/ 449 }); 450 451 t.describe('Audio.pauseAsync', () => { 452 t.it('pauses the sound', async () => { 453 await soundObject.loadAsync(mainTestingSource); 454 await soundObject.playAsync(); 455 await retryForStatus(soundObject, { isPlaying: true }); 456 await soundObject.pauseAsync(); 457 await retryForStatus(soundObject, { isPlaying: false }); 458 await soundObject.playAsync(); 459 await retryForStatus(soundObject, { isPlaying: true }); 460 }); 461 }); 462 463 t.describe('Audio.stopAsync', () => { 464 t.it('stops the sound', async () => { 465 await soundObject.loadAsync(mainTestingSource, { shouldPlay: true }); 466 await retryForStatus(soundObject, { isPlaying: true }); 467 await soundObject.stopAsync(); 468 await retryForStatus(soundObject, { isPlaying: false }); 469 }); 470 }); 471 472 t.describe('Audio.setPositionAsync', () => { 473 t.it('sets the position', async () => { 474 await soundObject.loadAsync(mainTestingSource); 475 await retryForStatus(soundObject, { positionMillis: 0 }); 476 await soundObject.setPositionAsync(1000); 477 await retryForStatus(soundObject, { positionMillis: 1000 }); 478 }); 479 }); 480 481 t.describe('Audio.setPositionAsync', () => { 482 t.it('sets the position', async () => { 483 await soundObject.loadAsync(mainTestingSource); 484 await retryForStatus(soundObject, { positionMillis: 0 }); 485 await soundObject.setPositionAsync(1000); 486 await retryForStatus(soundObject, { positionMillis: 1000 }); 487 }); 488 489 t.it('sets the position with tolerance', async () => { 490 await soundObject.loadAsync(mainTestingSource); 491 await retryForStatus(soundObject, { positionMillis: 0 }); 492 await soundObject.setPositionAsync(999, { 493 toleranceMillisBefore: 0, 494 toleranceMillisAfter: 0, 495 }); 496 await retryForStatus(soundObject, { positionMillis: 999 }); 497 }); 498 }); 499 500 t.describe('Audio.setVolumeAsync', () => { 501 t.beforeEach(async () => { 502 await soundObject.loadAsync(mainTestingSource, { volume: 1 }); 503 await retryForStatus(soundObject, { volume: 1 }); 504 }); 505 506 t.it('sets the volume', async () => { 507 await soundObject.setVolumeAsync(0.5); 508 await retryForStatus(soundObject, { volume: 0.5 }); 509 }); 510 511 t.it('sets the audio panning', async () => { 512 await soundObject.setVolumeAsync(0.5, 1); 513 await retryForStatus(soundObject, { volume: 0.5, audioPan: 1 }); 514 }); 515 516 const testVolumeFailure = (valueDescription, values) => 517 t.it( 518 `rejects if volume ${values.audioPan ? 'panning' : 'value'} is ${valueDescription}`, 519 async () => { 520 let hasBeenRejected = false; 521 try { 522 await soundObject.setVolumeAsync(values.volume, values.audioPan); 523 } catch (error) { 524 hasBeenRejected = true; 525 error && t.expect(error.message).toMatch(/value .+ between/); 526 } 527 t.expect(hasBeenRejected).toBe(true); 528 } 529 ); 530 531 testVolumeFailure('too big', { volume: 2 }); 532 testVolumeFailure('negative', { volume: -0.5 }); 533 534 testVolumeFailure('too small', { volume: 1, audioPan: -1.1 }); 535 testVolumeFailure('too big', { volume: 1, audioPan: 1.1 }); 536 }); 537 538 t.describe('Audio.setIsMutedAsync', () => { 539 t.it('sets whether the audio is muted', async () => { 540 await soundObject.loadAsync(mainTestingSource, { isMuted: true }); 541 await retryForStatus(soundObject, { isMuted: true }); 542 await soundObject.setIsMutedAsync(false); 543 await retryForStatus(soundObject, { isMuted: false }); 544 }); 545 }); 546 547 t.describe('Audio.setIsLoopingAsync', () => { 548 t.it('sets whether the audio is looped', async () => { 549 await soundObject.loadAsync(mainTestingSource, { isLooping: false }); 550 await retryForStatus(soundObject, { isLooping: false }); 551 await soundObject.setIsLoopingAsync(true); 552 await retryForStatus(soundObject, { isLooping: true }); 553 }); 554 }); 555 556 /*t.describe('Audio.setProgressUpdateIntervalAsync', () => { 557 t.it('sets update interval', async () => { 558 const onPlaybackStatusUpdate = t.jasmine.createSpy('onPlaybackStatusUpdate'); 559 await soundObject.loadAsync(mainTestingSource, { shouldPlay: true }); 560 await retryForStatus(soundObject, { isPlaying: true }); 561 soundObject.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); 562 await soundObject.setProgressUpdateIntervalAsync(100); 563 await new Promise(resolve => { 564 setTimeout(() => { 565 t.expect(onPlaybackStatusUpdate.calls.count()).toBeGreaterThan(5); 566 resolve(); 567 }, 800); 568 }); 569 }); 570 });*/ 571 572 t.describe('Audio.setRateAsync', () => { 573 let rate = 0; 574 let shouldError = false; 575 let shouldCorrectPitch = false; 576 let pitchCorrectionQuality = Audio.PitchCorrectionQuality.Low; 577 578 t.beforeEach(async () => { 579 const rate = 0.9; 580 581 const status = await soundObject.loadAsync(mainTestingSource, { rate }); 582 t.expect(status.rate).toBeCloseTo(rate, 2); 583 }); 584 585 t.afterEach(async () => { 586 let hasBeenRejected = false; 587 588 try { 589 const status = await soundObject.setRateAsync( 590 rate, 591 shouldCorrectPitch, 592 pitchCorrectionQuality 593 ); 594 t.expect(status.rate).toBeCloseTo(rate, 2); 595 t.expect(status.shouldCorrectPitch).toBe(shouldCorrectPitch); 596 t.expect(status.pitchCorrectionQuality).toBe(pitchCorrectionQuality); 597 } catch { 598 hasBeenRejected = true; 599 } 600 601 t.expect(hasBeenRejected).toEqual(shouldError); 602 603 rate = 0; 604 shouldError = false; 605 shouldCorrectPitch = false; 606 }); 607 608 t.it('sets rate with shouldCorrectPitch = true', async () => { 609 rate = 1.5; 610 shouldCorrectPitch = true; 611 }); 612 613 t.it('sets rate with shouldCorrectPitch = false', async () => { 614 rate = 0.75; 615 shouldCorrectPitch = false; 616 }); 617 618 t.it('sets pitchCorrectionQuality to Low', async () => { 619 rate = 0.5; 620 shouldCorrectPitch = true; 621 pitchCorrectionQuality = Audio.PitchCorrectionQuality.Low; 622 }); 623 624 t.it('sets pitchCorrectionQuality to Medium', async () => { 625 pitchCorrectionQuality = Audio.PitchCorrectionQuality.Medium; 626 }); 627 628 t.it('sets pitchCorrectionQuality to High', async () => { 629 pitchCorrectionQuality = Audio.PitchCorrectionQuality.High; 630 }); 631 632 t.it('rejects too high rate', async () => { 633 rate = 40; 634 shouldError = true; 635 }); 636 637 t.it('rejects negative rate', async () => { 638 rate = -10; 639 shouldError = true; 640 }); 641 }); 642 }); 643} 644