Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix AudioContext not resuming properly. #4462

Merged
merged 5 commits into from
Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 79 additions & 28 deletions src/sound/instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,15 @@ class SoundInstance extends EventHandler {
*/
this._lastNode = null;

/**
* Set to true if a play() request was issued when the AudioContext was still suspended,
* and will therefore wait until it is resumed to play the audio.
*
* @type {boolean}
* @private
*/
this._waitingContextSuspension = false;

this._initializeNodes();

/** @private */
Expand Down Expand Up @@ -573,15 +582,52 @@ class SoundInstance extends EventHandler {
}

/**
* Begins playback of sound. If the sound is not loaded this will return false. If the sound is
* already playing this will restart the sound.
* Attempt to begin playback the sound.
* If the AudioContext is suspended, the audio will only start once it's resumed.
* If the sound is already playing, this will restart the sound.
*
* @returns {boolean} True if the sound was started.
* @returns {boolean} True if the sound was started immediately.
*/
play() {
if (this._state !== STATE_STOPPED) {
this.stop();
}
// set state to playing
this._state = STATE_PLAYING;
// no need for this anymore
this._playWhenLoaded = false;
slimbuck marked this conversation as resolved.
Show resolved Hide resolved

// play() was already issued but hasn't actually started yet
if (this._waitingContextSuspension) {
return false;
}

// manager is suspended so audio cannot start now - wait for manager to resume
if (this._manager.suspended) {
this._manager.once('resume', this._playAudioImmediate, this);
this._waitingContextSuspension = true;

return false;
}

this._playAudioImmediate();

return true;
}

/**
* Immediately play the sound.
* This method assumes the AudioContext is ready (not suspended or locked).
*
* @private
*/
_playAudioImmediate() {
this._waitingContextSuspension = false;

// between play() and the manager being ready to play, a stop() or pause() call was made
if (this._state !== STATE_PLAYING) {
return;
}

if (!this.source) {
this._createSource();
Expand All @@ -605,11 +651,6 @@ class SoundInstance extends EventHandler {
this._currentTime = 0;
this._currentOffset = offset;

// set state to playing
this._state = STATE_PLAYING;
// no need for this anymore
this._playWhenLoaded = false;

// Initialize volume and loop - note moved to be after start() because of Chrome bug
this.volume = this._volume;
this.loop = this._loop;
Expand All @@ -621,15 +662,9 @@ class SoundInstance extends EventHandler {
this._manager.on('resume', this._onManagerResume, this);
this._manager.on('destroy', this._onManagerDestroy, this);

// suspend immediately if manager is suspended
if (this._manager.suspended) {
this._onManagerSuspend();
}

if (!this._suspendInstanceEvents)
if (!this._suspendInstanceEvents) {
this._onPlay();

return true;
}
}

/**
Expand All @@ -641,15 +676,20 @@ class SoundInstance extends EventHandler {
// no need for this anymore
this._playWhenLoaded = false;

if (this._state !== STATE_PLAYING || !this.source)
if (this._state !== STATE_PLAYING)
return false;

// store current time
this._updateCurrentTime();

// set state to paused
this._state = STATE_PAUSED;

// play() was issued but hasn't actually started yet.
if (this._waitingContextSuspension) {
return true;
}

// store current time
this._updateCurrentTime();

// Stop the source and re-create it because we cannot reuse the same source.
// Suspend the end event as we are manually stopping the source
this._suspendEndEvent++;
Expand All @@ -675,6 +715,14 @@ class SoundInstance extends EventHandler {
return false;
}

// set state back to playing
this._state = STATE_PLAYING;

// play() was issued but hasn't actually started yet
if (this._waitingContextSuspension) {
return true;
}

if (!this.source) {
this._createSource();
}
Expand All @@ -699,9 +747,6 @@ class SoundInstance extends EventHandler {
this.source.start(0, offset);
}

// set state back to playing
this._state = STATE_PLAYING;

this._startedAt = this._manager.context.currentTime;
this._currentOffset = offset;

Expand All @@ -726,9 +771,18 @@ class SoundInstance extends EventHandler {
stop() {
this._playWhenLoaded = false;

if (this._state === STATE_STOPPED || !this.source)
if (this._state === STATE_STOPPED)
return false;

// set state to stopped
const wasPlaying = this._state === STATE_PLAYING;
this._state = STATE_STOPPED;

// play() was issued but hasn't actually started yet
if (this._waitingContextSuspension) {
return true;
}

// unsubscribe from manager events
this._manager.off('volumechange', this._onManagerVolumeChange, this);
this._manager.off('suspend', this._onManagerSuspend, this);
Expand All @@ -743,14 +797,11 @@ class SoundInstance extends EventHandler {
this._startOffset = null;

this._suspendEndEvent++;
if (this._state === STATE_PLAYING) {
if (wasPlaying && this.source) {
this.source.stop(0);
}
this.source = null;

// set the state to stopped
this._state = STATE_STOPPED;

if (!this._suspendInstanceEvents)
this._onStop();

Expand Down
Loading