Skip to content

Commit

Permalink
fix(sounds): uncontrolled loop bleep playing on audio locked
Browse files Browse the repository at this point in the history
  • Loading branch information
romelperez committed Mar 28, 2021
1 parent 8bfd7cd commit f3cffac
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 10 deletions.
69 changes: 64 additions & 5 deletions packages/sounds/src/utils/createBleep/createBleep.test.ts
@@ -1,7 +1,38 @@
/* eslint-env jest */

import howler from 'howler';

import { createBleep } from './createBleep';

jest.mock('howler', () => {
const Howler = {
_audioUnlocked: true
};
class Howl {
constructor (settings: any) {
const self = this as any;
self.__testBleepHowlSettings = settings;
}

play (): void {}
stop (): void {}
playing (): void {}
state (): void {}
duration (): void {}
load (): void {}
unload (): void {}
}
return { Howler, Howl };
});

const howlerGlobalAPI: any = howler.Howler;
const unlockGlobalAudio = (): any => (howlerGlobalAPI._audioUnlocked = true);
const lockGlobalAudio = (): any => (howlerGlobalAPI._audioUnlocked = false);

beforeEach(() => {
unlockGlobalAudio();
});

test('Should create bleep with provided settings', () => {
const audioSettings = {
volume: 0.8,
Expand Down Expand Up @@ -71,6 +102,36 @@ test('Should always set shared sound id as number or undefined when playing (eve
expect(howlPlay).toHaveBeenCalled();
});

test('Should not play sound when global audio is locked', () => {
lockGlobalAudio();

const audioSettings = { volume: 0.8 };
const playerSettings = { src: ['sound.webm'] };
const bleep = createBleep(audioSettings, playerSettings);
const howlPlay = jest.spyOn(bleep._howl, 'play');
bleep.play('A');
expect(howlPlay).not.toHaveBeenCalled();
});

test('Should unlock bleep audio when it was locked and then unlocked to be playable', () => {
lockGlobalAudio();

const audioSettings = { volume: 0.8 };
const playerSettings = { src: ['sound.webm'] };
const bleep = createBleep(audioSettings, playerSettings);
const howlPlay = jest.spyOn(bleep._howl, 'play');
bleep.play('A');
expect(howlPlay).not.toHaveBeenCalled();

unlockGlobalAudio();

const bleepHowl: any = bleep._howl;
bleepHowl.__testBleepHowlSettings.onunlock();

bleep.play('A');
expect(howlPlay).toHaveBeenCalled();
});

test('Should allow stop play in shared sound only when it is already playing', () => {
const audioSettings = { volume: 0.8 };
const playerSettings = { src: ['sound.webm'] };
Expand All @@ -82,7 +143,6 @@ test('Should allow stop play in shared sound only when it is already playing', (
bleep.stop('A');
expect(howlPlay).toHaveBeenCalled();
expect(howlStop).toHaveBeenCalledTimes(1);
expect(howlStop).toHaveBeenNthCalledWith(1, 777);
});

test('Should not stop sound if it is not playing', () => {
Expand Down Expand Up @@ -150,7 +210,7 @@ test('Should not manage non-loop sound play/stop multiple calls to prevent race-
expect(howlStop).toHaveBeenCalled();
});

test('Should allow get the playing status in shared sound', () => {
test('Should allow to get the playing status', () => {
const audioSettings = { volume: 0.8 };
const playerSettings = { src: ['sound.webm'] };
const bleep = createBleep(audioSettings, playerSettings);
Expand All @@ -160,10 +220,9 @@ test('Should allow get the playing status in shared sound', () => {
expect(bleep.getIsPlaying()).toBe(true);
expect(howlPlay).toHaveBeenCalled();
expect(howlPlaying).toHaveBeenCalledTimes(1);
expect(howlPlaying).toHaveBeenNthCalledWith(1, 777);
});

test('Should allow get duration of sound', () => {
test('Should allow to get duration of sound', () => {
const audioSettings = { volume: 0.8 };
const playerSettings = { src: ['sound.webm'] };
const bleep = createBleep(audioSettings, playerSettings);
Expand All @@ -172,7 +231,7 @@ test('Should allow get duration of sound', () => {
expect(howlDuration).toHaveBeenCalledTimes(1);
});

test('Should allow get duration of sound', () => {
test('Should allow to unload sound', () => {
const audioSettings = { volume: 0.8 };
const playerSettings = { src: ['sound.webm'] };
const bleep = createBleep(audioSettings, playerSettings);
Expand Down
31 changes: 26 additions & 5 deletions packages/sounds/src/utils/createBleep/createBleep.ts
@@ -1,4 +1,4 @@
import { Howl } from 'howler';
import { Howler, Howl } from 'howler';

import {
BleepsAudioGroupSettings,
Expand All @@ -12,10 +12,23 @@ const createBleep = (audioSettings: BleepsAudioGroupSettings, playerSettings: Bl
...audioSettings,
...playerSettings
};
const howl = new Howl(settings);

// TODO: The Howler API does not provide a public interface to know if
// the browser audio is locked or not. But it has a private flag.
// This could potentially break this library if it changes unexpectedly,
// but there is no proper way to know.
const isGlobalAudioLocked = !(Howler as any)._audioUnlocked;

let isLocked: boolean = isGlobalAudioLocked;
let lastId: number | undefined;

const howl = new Howl({
...settings,
onunlock: () => {
isLocked = false;
}
});

// In a loop sound, if the sound is played by multiple sources
// (e.g. multiple components multiple times), to stop the sound,
// all of the play() calls must also call stop().
Expand All @@ -29,6 +42,14 @@ const createBleep = (audioSettings: BleepsAudioGroupSettings, playerSettings: Bl
howl.load();
}

// If the browser audio is locked, if the audio is played, it will be queued
// until the browser audio is unlocked. But if in-between the audio is stopped,
// the play is still queued. It is also accumulated, regardless of passing down
// the same playback id.
if (isLocked) {
return;
}

sourcesAccount[instanceId] = true;

// If the sound is being loaded, the play action will be
Expand All @@ -47,13 +68,13 @@ const createBleep = (audioSettings: BleepsAudioGroupSettings, playerSettings: Bl

const canStop = settings.loop ? noActiveSources : true;

if (canStop && howl.playing(lastId)) {
howl.stop(lastId);
if (canStop && howl.playing()) {
howl.stop();
}
};

const getIsPlaying = (): boolean => {
return howl.playing(lastId);
return howl.playing();
};

const getDuration = (): number => {
Expand Down

0 comments on commit f3cffac

Please sign in to comment.